spine-rigc 0.3.0 โ†’ 0.5.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
@@ -18,7 +18,10 @@ that can see that, and a run that skips it has verified nothing about the motion
18
18
  - Formats and CLI reference: [README.md](../README.md)
19
19
  - The rig spec's own source-level documentation: [`src/rig.ts`](../src/rig.ts)
20
20
  - The motion spec and emitted shapes: [`src/types.ts`](../src/types.ts)
21
- - What the format holds and rigc covers: [SPEC_COVERAGE.md](SPEC_COVERAGE.md)
21
+ - What the format holds and rigc covers: `docs/SPEC_COVERAGE.md` โ€” ๐Ÿšซ **not an
22
+ authoring input, and deliberately unlinked**: it inventories the benchmark corpus
23
+ skeleton by skeleton, so it is on the ladder run's forbidden list. Named here for a
24
+ maintainer, not offered to a run
22
25
  - Reproducing a shot you were given as pictures: **ยง8**, and read it *before* you
23
26
  start measuring rather than after; **ยง8.1** if the figure has more joints than you
24
27
  can measure one at a time; then **ยง9** for the loop that closes it
@@ -26,6 +29,22 @@ that can see that, and a run that skips it has verified nothing about the motion
26
29
  attachment, keying practice, curve kind, draw order โ€” sourced from Spine's own
27
30
  public documentation: **ยง10**
28
31
 
32
+ ๐Ÿ”’ **A ladder run reads this guide in full and does not follow its references out of
33
+ it.** The guide is allowed reading; not everything it cites is. Citations here are
34
+ provenance for a reader of record โ€” the loop that hit a trap, the issue that closed it โ€”
35
+ 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
37
+ document, take its numbered sections as the input, and leave its footprints to whoever
38
+ is maintaining it. The rule this states is that an **allowed-reading surface has to be
39
+ 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
41
+ `bench/runs/README.md`, *What a run may read* โ€” the prompt that starts a run quotes them
42
+ outright, which is the copy that binds.
43
+
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
+
29
48
  ## The vocabulary is Spine's
30
49
 
31
50
  Wherever rigc has no better abstraction it uses **Spine 4.3's own concept, its own
@@ -60,6 +79,7 @@ bun cli.ts check \
60
79
  --frames path/to/reference/frames
61
80
 
62
81
  # read the table โ†’ fix the spec โ†’ build again โ†’ check again
82
+ # โ†ณ read its per-frame column before its MAE โ€” ยง9.2
63
83
  ```
64
84
 
65
85
  `build` compiles, round-trips the result through `@esotericsoftware/spine-core`,
@@ -73,6 +93,18 @@ the animation is the one in the frames, and there is no assertion that could โ€”
73
93
  ยง9. The two run in that order because `check` needs artifacts on disk and `build`
74
94
  only writes them when the gate is green.
75
95
 
96
+ ๐Ÿšจ **Read `check`'s per-frame column before its MAE.** The table's headline figures
97
+ are the MAE and the slot drift, and a reader who came for those will skip the
98
+ `per-frame` line printed under them โ€” but that line is the only thing in this
99
+ toolchain that can see a **hold**, a **loop seam** or a **one-frame event**. Those
100
+ defects are cheap in every single frame and wrong only in the relation between two,
101
+ so an aggregate MAE, `diff` and the gate are all silent on them: a candidate can
102
+ slope a line through a frame pair the reference holds perfectly still across, or
103
+ end a cycle on a pose that is not the pose it began on, without moving a decimal
104
+ anywhere else in the loop. **ยง9.2** documents the column. It is named here because
105
+ ยง0 is where the loop is learned, and a run that opens a report for its chain table
106
+ can come away with the column unread.
107
+
76
108
  What the flags mean:
77
109
 
78
110
  | Flag | Meaning |
@@ -263,8 +295,8 @@ payloads in this guide are written to illustrate a field, never copied out of a
263
295
  reference export โ€” an example lifted from one would be handing an authoring agent an
264
296
  answer to the rung it is standing on, which is the rule ยง10.6 states and the honesty
265
297
  rule in [LADDER.md](LADDER.md) turns on. If a snippet here matches a reference file,
266
- that is a defect in this guide: report it. (It has happened โ€” 2026-08-23, see
267
- [`bench/runs/README.md`](../bench/runs/README.md), *What a run may read*.)
298
+ that is a defect in this guide: report it. (It has happened โ€” 2026-08-23; the incident
299
+ is recorded in `bench/runs/README.md`, *What a run may read*.)
268
300
 
269
301
  ### 3.1 `skeleton` โ€” the header
270
302
 
@@ -603,6 +635,26 @@ stepped.
603
635
  written for. โš ๏ธ What this does **not** protect you from is rounding your own
604
636
  times before you write them โ€” write `2/12`, not `0.1667`, and let the compiler
605
637
  do the quantising.
638
+ - ๐Ÿšจ **Nor does it protect a stepped key whose time is ALREADY on the 1e-6 grid.**
639
+ Rounding down leaves such a time exactly where you wrote it, and the sampler does
640
+ not arrive there: a player โ€” and `sampleAnimation`, and therefore `check` โ€” reaches
641
+ sample *i* by accumulating `1/fps` *i* times, which for many *i* lands a few ULPs
642
+ **below** `i/fps`. `2/12` is saved by the rule above precisely because it is *not*
643
+ on the grid; `0.25`, `0.5`, `0.75`, `1` and every other multiple of `0.25 s` is, and
644
+ a stepped key there sits above the sample that was meant to see it. On an
645
+ interpolated timeline that costs a few ULPs of value and nothing else. On a
646
+ **stepped** one it is the whole frame โ€” and on the last sample it is the whole
647
+ event, because there is no later sample to catch it. Measured on rung 5's 6.5 s
648
+ shot at 12 fps: **13 of its 78 sample times are affected** (f6, f15, f18, f21, f24,
649
+ f27, f60, f63, f66, f69, f72, f75, f78), and an attachment key written at the
650
+ declared duration `6.5` never fired at all against an accumulated
651
+ `6.499999999999994` โ€” which read as a frame-change disagreement the pose series had
652
+ 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
654
+ stepped timeline, write `T โˆ’ 1e-6` rather than `T`.** One grid step early cannot
655
+ reach the previous sample โ€” 83,333 ยตs away at 12 fps โ€” and is always seen by the
656
+ sample it was written for; one ULP late loses the frame. This is the same asymmetry
657
+ the rule above turns on, one grid step further in.
606
658
  - **No key may land past the animation's `duration`.** Nothing that plays the
607
659
  animation for the duration it declares ever reaches such a key, so it is a
608
660
  compile error โ€” checked on **every timeline**, not just on the latest key in the
@@ -956,17 +1008,111 @@ frames did not show the rest, and rendering like-for-like settled three more of
956
1008
  โ€” worth a measurable drop in window MAE, a convention the gate cannot see and the
957
1009
  measures can.
958
1010
 
959
- โš ๏ธ **The same test knows when to stay silent, and you have to let it.** Run on
960
- another shot, two orders came out **0.8 % apart over the whole shot and pointing
961
- opposite ways** โ€” a gap well inside the objective's own scatter, which
962
- is correctly *no answer*. A difference that small is not a quiet vote for the
963
- winner; it means the frames do not decide this edge, and you ship it on reasoning
964
- and say in the log that is what you did.
1011
+ **Score that comparison over the pixels where the two builds differ. A whole-shot
1012
+ figure is the wrong feature.** Two builds that differ only in slot order are
1013
+ **bit-identical everywhere the two slots do not overlap**, so a whole-shot MAE
1014
+ divides the evidence by the whole figure and by every frame that carries none of
1015
+ it. What survives that division sits inside the objective's own scatter โ€” real
1016
+ hypotheses and a deliberately reversed control alike land in there, pointing
1017
+ whichever way the noise does, and what has been condemned is the statistic and not
1018
+ the edge. โ‡’ Take the pixels where the two renders differ **at all** and score both
1019
+ builds over exactly that set. Nothing outside it can contribute, so the dilution is
1020
+ gone by construction, and the reading needs no knowledge of which parts are involved
1021
+ โ€” it is the same mechanical test on any structural pair. Read a frame-by-frame tally
1022
+ beside the figure too, because an edge the frames really decide wins shot after shot
1023
+ rather than on a couple of them.
1024
+
1025
+ โญ **That dilution has a *temporal* cousin, and it bites inside a single shot's own
1026
+ per-frame fit.** The paragraph above is about two builds and a whole-shot figure; this is
1027
+ about one build and a whole-*figure* objective. Where a passage's motion is **a small
1028
+ part moving against a large, nearly still body** โ€” a hand, a head, a prop, while
1029
+ everything else holds โ€” the moving part is a tiny share of the ink, so a whole-figure
1030
+ score is dominated by the still majority. Every frame then reports a good number
1031
+ *individually*, the fit converges, and the passage comes out **static**: the mover was
1032
+ never worth enough of the objective to pull the search toward it.
1033
+
1034
+ โš ๏ธ **Nothing else in the loop catches this.** The MAE is fine, the drift is fine on every
1035
+ part that is not moving, and `validate` and `diff` never look at a rendered frame. What
1036
+ does see it is ยง10.3's change column, in its **under-change** direction โ€” and by the time
1037
+ it tells you, the poses are already wrong, because a key plan cannot add motion the poses
1038
+ do not have.
1039
+
1040
+ โ‡’ **Weight the objective by the reference's own frame-to-frame change.** Build a mask
1041
+ from where the reference *changes* between the two frames bracketing the one you are
1042
+ fitting, and weight the score by it โ€” so the pixels that carry the passage's motion carry
1043
+ the passage's objective. It costs one extra difference per frame, needs nothing but the
1044
+ frames, and it turns an untrackable passage into an ordinary one.
1045
+
1046
+ ๐Ÿ“Œ **Read the mask itself before you trust the fit, because it also tells you what is
1047
+ actually moving** โ€” which is frequently not what the shot looks like it is about. A
1048
+ passage that reads as one limb waving can turn out to carry most of its change somewhere
1049
+ else entirely (a body-wide micro-rocking, a shadow, a trailing part), and a fitter aimed
1050
+ at the limb would have been chasing the minority of the evidence. The mask is the cheap
1051
+ way to find that out first.
1052
+
1053
+ **Calibrate the band with a control on an edge the brief has already settled by
1054
+ measurement.** Run the same test on that edge, read how far apart the two builds
1055
+ come out over the pixels that decide it, and treat that separation as the scale a
1056
+ real answer is measured against. On the deciding pixels a settled edge separates by
1057
+ a wide margin where the whole-shot figure had it inside its own noise โ€” which is
1058
+ what lets an edge the frames show no interior detail on stop being unanswerable and
1059
+ start being an edge the null-result rule below has no business firing on.
1060
+
1061
+ โš ๏ธ **A control that fails may be a wrong control โ€” read the per-frame rows before
1062
+ you condemn the hypothesis.** A control is a **build**, and a build differs from
1063
+ base in everything the change implies, not only in the thing you meant to change:
1064
+ send one part behind another and it goes behind everything drawn between them too,
1065
+ so what you actually ran is one reversed edge plus several asserted ones. The
1066
+ aggregate will not say so, and it can favour the variant while the per-frame rows
1067
+ give base *every one* of the frames that carry most of the deciding pixels. That
1068
+ split โ€” an aggregate one way, a consistent per-frame tally the other โ€” is the
1069
+ signature of a control that asserts more than one thing, and reading the aggregate
1070
+ alone condemns an edge the brief settles by measurement. The rule the run protocol
1071
+ carries from the other side is the same one: a control that returns an impossible
1072
+ number has told you something, so read the number rather than the pass or fail.
1073
+
1074
+ โš ๏ธ **A render-back sweep whose spread is inside the objective's own scatter is
1075
+ *no answer*, not a weak one.** Rendering candidates back and keeping the best
1076
+ number is not a draw-order trick โ€” it is how any structural choice the frames
1077
+ might decide gets decided, a scale, an offset, an attachment kind, an order โ€” and
1078
+ every one of those sweeps can come back null. **Estimate the objective's own
1079
+ scatter first, or the sweep is not readable at all** โ€” a spread smaller than that
1080
+ is noise wearing a decimal point. Two orders on one ladder shot came out **0.8 %
1081
+ apart over the whole shot and pointing opposite ways**, and a later run swept
1082
+ **three** structural choices and landed inside that scatter on every one of them.
1083
+ A difference that small is not a quiet vote for the winner; it means the frames do
1084
+ not decide this, and there are two honest ways on:
1085
+
1086
+ - **find a second, independent way to get the number** โ€” often by measuring the
1087
+ *art* instead of the render, which needs no build at all. Two of those three
1088
+ sweeps were settled that way.
1089
+ - **or ship it on reasoning, and say in the log that is what you did.** The third
1090
+ one was. What makes that honest is the record โ€” a number that arrived as an
1091
+ argument must not later be read as a measurement.
965
1092
 
966
1093
  And the general form of all three: **when a reading implies a key, look for a second
967
1094
  way to get the same number before you author it.** A wrong measurement costs one
968
1095
  spurious key; a wrong measurement you believed costs the shape of the whole shot.
969
1096
 
1097
+ โš ๏ธ **A part that grows about a pivot that is not its own centre reads as a part
1098
+ that moves.** Fit a scale about the region centre when the reference scales it about
1099
+ somewhere else, and the residual stays *plausible* while the fitted centre walks
1100
+ along with the fitted scale โ€” so the series looks like a translate you measured
1101
+ rather than a pivot you did not model. The tell is that shape and nothing else: **a
1102
+ per-frame centre that moves monotonically with the fitted scale is an unmodelled
1103
+ pivot.** Read it as motion and you author a translate timeline the reference does
1104
+ not have; read it as a pivot and it is an attachment offset (ยง3.4's `x`/`y`) with
1105
+ the bone's own scale carrying both the size and the centre drift โ€” *one* keyed
1106
+ property, which is also what an editor rig has. Recover the offset by sweeping it
1107
+ against the frames where the part is unoccluded and taking the minimum; the minimum
1108
+ is sharp, and it is a structural constant rather than a per-frame value, so a
1109
+ handful of frames settle it. Measured on a shot with four such parts: MAE **3.13 โ†’
1110
+ 1.95** with not one key value re-measured, and those parts' chains from 10โ€“13
1111
+ `MAE in it` down to 1.3โ€“8.8. What made it believable rather than a lucky fit is the
1112
+ second trap's own cross-check โ€” the four offsets came out the same **fraction of each
1113
+ part's own height**, from four independent fits, which is the quantity that had to
1114
+ agree between them and did.
1115
+
970
1116
  **A value is easier to get right than a curve.** The three traps above are all
971
1117
  about measuring a *value*, and both ladder runs so far found that the values came
972
1118
  out right early: rung 1's key values were exact at every keyframe on the second
@@ -1060,6 +1206,58 @@ the other two. Where a chain ends in something whose position you can actually s
1060
1206
  a hand, a foot, a held prop โ€” scan the two links above it as a **pair**, over the
1061
1207
  grid. That is the product of two ranges on a handful of chains, not on every bone.
1062
1208
 
1209
+ โš ๏ธ **Two whole chains can share a minimum, and no paired scan reaches that one.**
1210
+ The case above is two knobs in one chain. The harder one is two *chains* sitting in
1211
+ the same pixels: an arm and the prop it holds lying across the part of the frame the
1212
+ reference fills with the legs. Every leg knob that would carry a leg there finds the
1213
+ pixels already inked and reports no improvement โ€” correctly, on the objective it was
1214
+ given โ€” and the frame keeps its limbs in a **different configuration** rather than a
1215
+ slightly wrong one. Pairing cannot help here, because the two knobs are in different
1216
+ chains and pairing every chain with every other is the whole product.
1217
+
1218
+ **What reaches it is cheap: more than one start, screened coarsely.** Assemble a
1219
+ handful of candidate poses for the frame โ€” the incumbent, the two neighbouring
1220
+ frames' solutions, a few poses spread across the shot, the setup pose โ€” run all of
1221
+ them through the **coarse levels only**, and take the best two through the full
1222
+ schedule. **Keep the incumbent among the candidates**, so a frame can only improve
1223
+ on what it already had. The cost is a multiple of the coarse pass rather than of the
1224
+ fit, and the neighbour seed below is one start out of that set rather than a rule of
1225
+ its own. โš ๏ธ It also measures how far from converged a single-start fit can be while
1226
+ reporting success: repeat the *identical* search on one frame from different starts
1227
+ and the numbers walk down, step after step. That is not a tolerance being tightened,
1228
+ it is a different basin each time โ€” so a search that stopped improving is evidence
1229
+ about the start it was given and about nothing else.
1230
+
1231
+ **Cross-shot starts, for a configuration a shot cannot reach from its own frames.**
1232
+ Where every pose in one shot holds the prop low and the reference holds it out
1233
+ level, every start drawn from that shot's own frames is on the wrong side of the
1234
+ same two-chain minimum, and multi-start inside the shot barely moves the number.
1235
+ These shots are states of one character, so a configuration this shot never visits
1236
+ may be sitting in another one โ€” take the start from there. โ‡’ Borrow **only the bones
1237
+ of the chain in question**, never a whole foreign pose: a foreign pose puts the legs
1238
+ where this shot never goes, and the rest of the search then spends itself fighting
1239
+ what the borrow brought with it.
1240
+
1241
+ ๐Ÿšจ **Before you fit a chain at all, check that it can *reach* the extremes the shot
1242
+ visits โ€” a reach deficit is invisible to every per-frame fit.** This is the precondition
1243
+ the borrow rule assumes and the loop does not check. If a chain's segment lengths are
1244
+ short โ€” read off a pose where the chain is **folded**, which is the easiest reading to
1245
+ take and the one most likely to be wrong โ€” then every frame where the chain is folded
1246
+ fits beautifully, and the fitter *silently absorbs* the deficit on every other frame by
1247
+ rotating the parts it does have. Nothing reports a failure. The number is merely a little
1248
+ worse everywhere, which reads like an ordinary residual, until a passage needs the full
1249
+ extension and then no start converges anywhere near it โ€” and multi-start does not help,
1250
+ because the pose being searched for is **outside the chain's reachable set**.
1251
+
1252
+ โ‡’ **The check is arithmetic and needs no fit.** Take the chain's total reach from your own
1253
+ rig; take the longest excursion the shot's own frames show that chain's end travelling โ€”
1254
+ a pendulum's full swing, a limb's extreme, a prop's sweep โ€” and compare. If the shot asks
1255
+ for markedly more than the chain has, the rig is wrong and no amount of searching will say
1256
+ so. โญ **A frames-side reading beats a rig-side one here**: the shot's own extremes are a
1257
+ measurement, while segment lengths taken off a folded pose are an estimate โ€” so when they
1258
+ disagree, suspect the estimate. And do this **per chain, before its first fit**, because
1259
+ the surgery to fix it invalidates every pose already fitted with the short chain.
1260
+
1063
1261
  **Re-fit the setup pose against frames drawn from every shot, not against one.** Every
1064
1262
  animation is measured from the setup pose, so an error in it is an error in all of
1065
1263
  them โ€” and it is exactly the error one frame cannot show you. Fit an attachment's
@@ -1072,6 +1270,47 @@ against a handful of frames drawn from **every** animation at once, and hold it
1072
1270
  while the per-frame poses are fitted. It is the spread that identifies it โ€” a
1073
1271
  sequence of single-frame fits, one per shot, is not the same thing.
1074
1272
 
1273
+ ๐Ÿšจ **That rule is not sufficient for a *joint*, and the difference is not a matter of
1274
+ degree.** An attachment offset is identified by a spread of *rotations*; a **pivot** โ€” the
1275
+ point one bone turns about relative to its parent โ€” is identified only by frames whose
1276
+ **relative rotation across that joint actually differs**. So a spread can draw frames from
1277
+ every single shot, satisfy the paragraph above to the letter, and still be
1278
+ **ill-conditioned**: if every shot holds that joint at much the same relative angle, the
1279
+ pivot is barely constrained, and a wrong one re-solves far away *at equal residuals*. Equal
1280
+ residuals is the trap โ€” nothing in the fit reports a problem, because there genuinely is no
1281
+ better answer within the data you gave it.
1282
+
1283
+ ๐Ÿšซ **And a structural descent that holds the fitted poses fixed cannot recover a
1284
+ mis-triangulated pivot at all.** This is the part worth internalising, because it looks
1285
+ like the obvious repair and it is inert: the per-frame poses were *fitted against the wrong
1286
+ pivot*, so they have already absorbed its error. Move the pivot with those poses held and
1287
+ every frame gets worse; hold the pivot and refit the poses and they re-absorb it. **The
1288
+ gradient at fixed poses points nowhere**, so the descent reports convergence on the wrong
1289
+ geometry โ€” and multi-start does not help either, because the defect is not a basin you
1290
+ failed to reach, it is a parameter the objective is no longer a function of.
1291
+
1292
+ โ‡’ **Triangulate a joint from part template matches across *configurations*, not from the
1293
+ whole-figure objective.** Match the two parts the joint connects โ€” each is its own art file
1294
+ and its own reading โ€” on frames that put the joint in **genuinely different relative
1295
+ angles**, and solve for the one point that is fixed in both parts' own coordinates. Then
1296
+ refit the poses against the corrected pivot. Two practical notes:
1297
+
1298
+ - โญ **"Different configurations" means what the shot list looks like, not how many frames
1299
+ you took.** A figure standing, walking and running may hold one joint at nearly the same
1300
+ angle throughout; a figure **lying down**, or inverted, or reaching across itself, is what
1301
+ makes that joint observable. Pick frames for *angular diversity across the joint*, and if
1302
+ the shot list has only one configuration, say in the log that the pivot is a prior.
1303
+ - ๐Ÿ“Œ **Check the conditioning rather than trusting the fit**: re-solve the joint from a
1304
+ subset that excludes the diverse configurations and see how far the answer moves. If it
1305
+ moves a long way at comparable residuals, the diverse frames were carrying the whole
1306
+ identification โ€” which is exactly the state in which an earlier triangulation goes wrong
1307
+ silently.
1308
+ - โš ๏ธ **Sequence matters, because the surgery invalidates work.** Correcting a pivot
1309
+ invalidates every pose fitted under the old one, so do it **before** the per-frame fitting
1310
+ budget is spent, not after. When it has to be done late, expect to re-settle every channel
1311
+ hung off that joint โ€” and freeze the ones that are not, so the two effects stay separable
1312
+ in the record.
1313
+
1075
1314
  **Seed each frame's search from its neighbour's solution โ€” as one start among the
1076
1315
  full-range scans, never instead of them.** Adjacent frames are adjacent poses, so the
1077
1316
  answer next door is a better first guess than the middle of any range, and it costs one
@@ -1126,6 +1365,23 @@ not a quiet one.** The matcher refuses to name a distance past the part's own si
1126
1365
  (ยง9.2), so a limb far enough out reports no match rather than a large number โ€” read
1127
1366
  that beside a high figure per pixel as the strongest signal the table has.
1128
1367
 
1368
+ โš ๏ธ **Excess adjacency change has a second diagnosis, and the rule above assumes the
1369
+ first.** *A limb has left its place* is one cause โ€” a fit that teleported, which is what
1370
+ a blank drift and a high figure per pixel together point at. The other is **two
1371
+ independent per-frame residuals adding**: every pose inside its own accuracy, nothing
1372
+ lost, and the *difference* between two neighbours nonetheless several times the
1373
+ reference's. The fixes are opposite โ€” the first wants the search bounded or restarted,
1374
+ the second wants the neighbouring poses drawn toward each other (ยง10.3's own note on
1375
+ this) โ€” so guessing costs a round either way.
1376
+
1377
+ โ‡’ **Separate them by asking how much freedom the neighbour-mean step actually had.**
1378
+ Measure, over every neighbouring pair in the shot, how many of those steps your own
1379
+ constraints left **free** to move: if the answer is a percent or two of them, then the
1380
+ search was not free to teleport anything, and the excess is residuals adding rather than
1381
+ a lost limb. It is one count over data the
1382
+ fit already produced, and it is worth more than an afternoon of restarts aimed at the
1383
+ wrong cause.
1384
+
1129
1385
  **What comes out is a pose per frame, and a pose per frame is not a key.** Two things
1130
1386
  decide what survives the reduction, and **ยง10.3** states both: declare one tolerance
1131
1387
  in pixels at the end of what each bone swings rather than a figure in degrees, and
@@ -1153,19 +1409,42 @@ to fit one framing across every set instead of one each, `--all-frames` to list
1153
1409
  every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
1154
1410
  per-slot report.
1155
1411
 
1156
- โš ๏ธ **A frame set may be contact-sheets-only.** `check` only reads `fNNNN.png`
1157
- files โ€” a committed reference set that ships a contact sheet plus a couple of
1158
- stills (rung 2's does: `f0000.png` and `f0310.png` per animation, the rest folded
1159
- into `contact.png` so a 311-frame shot does not commit 311 near-duplicate PNGs)
1160
- reports `frames 2 on disk, candidate samples 311, 2 compared` and means it: `check`
1161
- compared exactly the committed stills, not the shot. That is not a defect to author
1162
- around โ€” the frame count line says so rather than pretending a fuller comparison
1163
- happened โ€” but it does mean a clean `check` table on a contact-sheet-only set says
1164
- nothing about the frames between the stills. Whole-shot fidelity against a contact
1165
- sheet needs a tile-wise comparison against the sheet's own grid, which `check` does
1166
- not do yet (issue #36);
1167
- [`bench/runs/2026-08-23-rung2-2/sheetcheck.ts`](../bench/runs/2026-08-23-rung2-2/sheetcheck.ts)
1168
- is a working prototype, built in-run for exactly this gap.
1412
+ โญ **A frame set may ship a contact sheet instead of every frame, and the sheet is
1413
+ compared too.** A long shot does not commit 311 near-duplicate PNGs: rung 2's sets
1414
+ ship `f0000.png` and `f0310.png` plus a `contact.png` holding all 311 sampled
1415
+ frames, and spineboy's `@30fps` sets do the same. The frame table still says
1416
+ `frames 2 on disk, candidate samples 311, 2 compared` โ€” those are the files โ€” and a
1417
+ **`sheet` line under it** carries the other 309: your candidate sampled at the set's
1418
+ own rate, rendered into the same box the frames above were at the sheet's own scale,
1419
+ and compared tile by tile (issue #36 โ€” the gap a rung-2 run had already prototyped a
1420
+ comparator for, in its own working directory, because the tool could not see its shot).
1421
+
1422
+ โš ๏ธ **Read it as a series, not as one number** โ€” ยง9.2. And note what it does not
1423
+ carry: MAE only. The `ฮ”px` / `ref ฮ”` thresholds are calibrated at frame scale and a
1424
+ tile has a fraction of a frame's pixels, so the per-frame change measure stays on
1425
+ the committed stills, where it reports `no two compared frames are adjacent` and
1426
+ means it.
1427
+
1428
+ โญ **Do not treat such a set's two stills as bookends. They are full-resolution frames
1429
+ at their own rate, and one of them is routinely a pose no other set on disk carries.**
1430
+ The temptation is to read a strided set as *a sheet, plus two files that fix the
1431
+ framing* โ€” the sheet is where the shot is, so the stills look like plumbing. But the
1432
+ last still is the animation's **own last sample at that rate**, and a finer rate lands
1433
+ on a different instant: a shot whose length is not a multiple of the coarse interval
1434
+ ends *between* two coarse samples, so the coarse set's last frame is not the end of the
1435
+ shot and the finer set's is. If the shot is still moving there โ€” and an end pose usually
1436
+ is the part that moves most โ€” that pose exists in exactly one file, at full resolution,
1437
+ and it is worth fitting like any other frame.
1438
+
1439
+ โ‡’ **Two consequences for a run.** โ‘  **Fit every committed still**, at every rate, and
1440
+ do not let a "sheets are for timing" habit skip them; a pose you never fitted is a pose
1441
+ you guessed, and a hold written across the gap because nothing on disk contradicted it
1442
+ is a **fabrication** rather than a simplification. โ‘ก This is the same fact a brief
1443
+ states from the timing side when it warns you against declaring the coarse set's
1444
+ rounded length: the rounding and the missing pose are one arithmetic, seen twice. If
1445
+ your shot's length is not a whole number of coarse intervals, expect **both** โ€” a
1446
+ duration the coarse sidecar understates, and a terminal pose only the finer set shows
1447
+ you.
1169
1448
 
1170
1449
  `--fps <n>` exists for frame sets that have no `frames.json` beside them, which are
1171
1450
  sets rendered before the sidecar existed: it gives the rate those frames were
@@ -1222,8 +1501,10 @@ question first: *do this set's own drawn pixels land in the box `frames.json`
1222
1501
  records?* The sets that do are measured in that box, which is exact โ€” it is not an
1223
1502
  estimate of where the frames were drawn, it is where they were drawn โ€” and nothing
1224
1503
  another set does can move them. The sets that do not are measured in **one shared
1225
- framing** fitted across every set, printed as the header's `shared box` line. Each
1226
- set says which it got on its own `framed to` line.
1504
+ framing** fitted across every set, printed as the header's `shared box` line, plus
1505
+ their own whole-pixel MAE refinement off it (ยง9.2) โ€” the fit is shared because more
1506
+ frames condition it better; the constant offset it still leaves is per set, and
1507
+ measured per set. Each set says which it got on its own `framed to` line.
1227
1508
 
1228
1509
  Why the split falls there, both halves measured on an 8-shot character (147 frames):
1229
1510
 
@@ -1278,6 +1559,13 @@ your run ceasing to be an honest authoring run. It is a loop, in the way `build`
1278
1559
  a loop. `bench` and `diff` against a rung's export are not โ€” they read the answer,
1279
1560
  and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach once.
1280
1561
 
1562
+ ๐Ÿ“Œ **That is also why the MAE figures quoted through this section stay.** Every one of
1563
+ them is a candidate's own reading against rendered frames โ€” the exam question, not the
1564
+ answer key โ€” so none of them narrows a reference-side measure, and a guide that censored
1565
+ them would be teaching less for no gain in honesty. The criterion is under *The honesty
1566
+ rule* in [LADDER.md](LADDER.md) (issue #158); what it *does* seal is a score written
1567
+ over a reference's own count, and no such figure appears here.
1568
+
1281
1569
  ๐Ÿšจ **If you drive the runtime yourself, a bone's local transform lives on
1282
1570
  `bone.pose`.** A shot whose poses have to be *fitted* rather than read sends you
1283
1571
  past `check` and into your own render loop over
@@ -1303,8 +1591,8 @@ file, the next build reads those as zero, `validate` is green and `check` runs.
1303
1591
  โ‡’ **A `null` in your own placements dump is the signature of having read
1304
1592
  `bone.data` directly** โ€” nothing in this format is ever legitimately null. On
1305
1593
  spineboy it cost the candidate MAE 13.0 โ†’ 114.6 with a green gate throughout
1306
- ([`bench/runs/2026-08-23-spineboy-2/LOOP.md`](../bench/runs/2026-08-23-spineboy-2/LOOP.md),
1307
- ยง4.1). Note that the two names are not the same thing: `bone.data.setupPose` is
1594
+ (measured in a spineboy attempt's own loop log, ยง4.1). Note that the two names are not
1595
+ the same thing: `bone.data.setupPose` is
1308
1596
  the setup transform, while `bone.setupPose()` on a `Bone` is the method that
1309
1597
  resets `bone.pose` back to it.
1310
1598
 
@@ -1315,8 +1603,7 @@ stores; what gets drawn is that stored quad โ€” `computeWorldVertices` reads
1315
1603
  error nor a move. **Call `attachment.updateSequence()` after every write**, or
1316
1604
  every frame renders the quad it was loaded with. A setup fit on spineboy ran
1317
1605
  4,500 renders and reported the same number for all of them before this surfaced
1318
- ([`bench/runs/2026-08-23-spineboy-2/LOOP.md`](../bench/runs/2026-08-23-spineboy-2/LOOP.md),
1319
- ยง4.2).
1606
+ (the same loop log, ยง4.2).
1320
1607
 
1321
1608
  โ‡’ **An MAE that is identical across every pose, and that does not move for any
1322
1609
  parameter you sweep โ€” a bone's local transform, an attachment's offsets โ€” is one
@@ -1324,6 +1611,145 @@ of these inert writes and not a wrong animation.** The parameter you swept was
1324
1611
  never read; a wrong rig moves the number, a write to a field nothing reads
1325
1612
  cannot.
1326
1613
 
1614
+ ๐Ÿšจ **And the mirror image of all three: an objective that *improves by removing the
1615
+ subject*.** The traps above are each *"the number will not move"*, and they train you
1616
+ to distrust a still figure. The twin is a figure that moves, in the right direction,
1617
+ for the wrong reason โ€” and it is the more dangerous one, because progress is what it
1618
+ looks like.
1619
+
1620
+ The shape is arithmetic, not a bug. **Any symmetric error over two silhouettes charges
1621
+ a mismatch in both directions**, so it charges your ink that the reference has none
1622
+ under *and* the reference's ink you leave bare. Give it a candidate that draws
1623
+ **nothing** and only the second term survives: the score is the reference's own ink,
1624
+ once, and it is a *finite, respectable-looking number*. A part that is present but
1625
+ badly posed pays both terms and can score **worse than absence**. โ‡’ A search with a
1626
+ free enough range finds the cliff and walks off it, and every step of the walk reports
1627
+ as an improvement. Synthetic illustration of the whole failure in three rows โ€” one
1628
+ part, one objective, nothing else changed:
1629
+
1630
+ | what the candidate does | part error |
1631
+ | --- | --- |
1632
+ | posed roughly right | 2.15 |
1633
+ | posed badly โ€” overlapping the wrong reference ink | 2.48 |
1634
+ | **translated clean off the frame** | **1.00** |
1635
+
1636
+ The search reports **1.00 against 2.15** and calls it a 53 % gain. What it found was
1637
+ the absence of a subject. โš ๏ธ **1.00 is not a coincidence in that table, it is the
1638
+ construction**: absence pays the reference's ink exactly once, so on any objective
1639
+ normalised by that ink, *"draw nothing"* sits at 1.0 by arithmetic โ€” which is why it is
1640
+ worth evaluating deliberately rather than discovering.
1641
+
1642
+ โ‡’ **Four defences, and the first is the cheap one.**
1643
+
1644
+ - **Bound the search to the frame.** A part cannot legitimately leave the picture on a
1645
+ shot whose frames all draw it, so a translation range wide enough to exit the
1646
+ viewport is a range that contains a false optimum. Bound each parameter by what the
1647
+ frames can *show*, not by what the format permits.
1648
+ - โš ๏ธ **And its converse, which is the easier half to get wrong: a bound has to
1649
+ *reach* what the frames show, not merely stop where they stop.** The two failures
1650
+ look nothing alike โ€” a bound that is too wide loses a fit to the cliff, a bound
1651
+ that is too narrow loses one to a wall it never reports hitting. Bounding a
1652
+ vertical channel to the range the *rest* pose occupies is the classic case: a shot
1653
+ that drops its subject in from hundreds of pixels above the standing pose puts its
1654
+ own entry outside the search entirely, and the fitter returns the best pose *it was
1655
+ allowed*, which is the top of the box, silently. โ‡’ **Take each channel's range from
1656
+ the brief and the frames โ€” the extremes the shot actually visits โ€” and then check
1657
+ afterwards how many converged values are sitting on a bound.** A knob resting
1658
+ exactly on its limit is the signature, and it costs one line to print.
1659
+ - **Assert the part is drawn, every iteration.** Count your own ink for that part and
1660
+ reject any candidate whose count is zero or a small fraction of the reference's. This
1661
+ is one comparison and it makes the cliff unreachable rather than merely unattractive.
1662
+ - โš ๏ธ **Write it at the resolution the level is actually being evaluated at.** On a
1663
+ coarse-to-fine pyramid, a threshold expressed in full-resolution pixel counts
1664
+ refuses **every** coarse pose โ€” and a rejection that fires on everything is
1665
+ indistinguishable from an objective with no gradient. You get `Infinity`, or a
1666
+ figure far worse than the same search reaches with the assert switched off, and
1667
+ nothing in either says *"your guard is the problem"*. โ‡’ Express the count as a
1668
+ **fraction of the reference's ink at that same level**, so the test means the same
1669
+ thing at every rung of the pyramid.
1670
+ - **Charge ink that leaves the window, because the cliff has a second entrance.** If
1671
+ your objective is computed inside a window around the reference's own drawn box โ€”
1672
+ and it usually is, since that is what makes it cheap โ€” then ink outside the window
1673
+ costs **nothing**, and the three defences above do not reach that: the part is still
1674
+ drawn, its count is still healthy, and the score still falls. The fitter hangs a part
1675
+ a few hundred pixels below the frame and reports progress every step. โ‡’ **Count your
1676
+ own ink further than a small margin outside the reference's drawn box and charge it.**
1677
+ One line, in the same place as the ink count, and it closes the entrance the bound
1678
+ closes only when the bound happens to be tight enough.
1679
+ - **Read the objective's floor before you trust its direction.** Evaluate *"draw
1680
+ nothing"* once, deliberately, and keep the figure. Any score at or below it is the
1681
+ cliff, whatever the search says โ€” and if your best honest pose is *above* that floor,
1682
+ the objective is ranking absence over effort and needs an asymmetry (charge bare
1683
+ reference ink more than stray candidate ink) before it is safe to optimise against.
1684
+
1685
+ ๐Ÿ“Œ **`check` itself is not exposed to this** โ€” its `MAE in it` and `share` columns
1686
+ divide over the **reference's** own drawn pixels and a chain that draws nothing reads
1687
+ 0 % on 0 slots, which ยง9.2 says is the loudest row in the table and not the quietest.
1688
+ The trap lives in the objectives **you** write inside a fit, where the denominator is
1689
+ yours to choose.
1690
+
1691
+ ๐Ÿšจ **The cliff's nearest cousin, and the one that survives all four defences:
1692
+ *sacrificial cover*.** Every defence above protects a part from being **removed**. None
1693
+ protects a part from being **moved somewhere wrong on purpose**. A whole-figure objective
1694
+ scores one number over every pixel, so when part A is mis-placed and leaves reference ink
1695
+ bare, the cheapest available improvement is frequently to drag **part B off its own correct
1696
+ place to cover that ink**. Both parts are drawn, both counts are healthy, nothing leaves
1697
+ the window โ€” and the score genuinely falls, because covering bare ink is worth more to a
1698
+ blunt objective than B's own displacement costs it.
1699
+
1700
+ โš ๏ธ **What makes it expensive is that the objective is not lying.** The pose it prefers
1701
+ really is better *by that measure*. So the loop offers no signal at all: the fit converges,
1702
+ the number improves, and what you have is one part visibly out of place standing in for
1703
+ another. It surfaces later as a **drift** on the sacrificed part โ€” a slot several pixels
1704
+ from where the frames put it inside a pose whose overall figure looks fine โ€” which is the
1705
+ one measure that reads parts individually.
1706
+
1707
+ โ‡’ **Two ways to catch it, and the first is nearly free.**
1708
+
1709
+ - **Read a per-part residual beside the composite, never only the composite.** Score each
1710
+ part against its own template match as well, and flag any frame where the composite
1711
+ improves while a part's own residual worsens. That divergence *is* the signature; the
1712
+ composite alone cannot express it.
1713
+ - **Seed the parts analytically from their own measured features, then refine jointly with
1714
+ the sacrificed part pinned.** If a part's place is independently measurable โ€” a colour
1715
+ feature, a template peak, a contact row the brief gives you โ€” put it there first rather
1716
+ than letting the composite negotiate it, and hold the part that was being abused fixed
1717
+ while the rest re-settles.
1718
+
1719
+ โš–๏ธ **Expect the corrected pose to score *worse* on the composite, and record that as a
1720
+ trade.** A few percent worse on your own objective while decisively better on every
1721
+ frame-derived placement instrument is the **expected** shape of this repair, not a
1722
+ regression โ€” the composite's preference was the defect. Declare an accept threshold before
1723
+ you need it, say how often you used it, and name the frames. โญ **And prefer the
1724
+ frame-derived instruments when they disagree with the composite about a single part's
1725
+ place**: the composite is one number over everything, while a template match on that part's
1726
+ own art is a measurement of the thing in question.
1727
+
1728
+ ๐Ÿšจ **One more inert-write trap, and it is on the way *out* of the fit rather than
1729
+ inside it: your compiled animation is not your pose series.** Everything above is about
1730
+ a search that reads the wrong thing; this is about a search that was right and an
1731
+ emission that was not. The formats differ in a way that is easy to miss โ€” **a translate
1732
+ key is an offset from the setup pose, while a fitter almost always drives the absolute
1733
+ local position** โ€” so writing the fitted numbers straight into keys applies the setup
1734
+ offset a second time and displaces the whole figure by it.
1735
+
1736
+ โš ๏ธ **What makes it expensive is how it presents.** `build` is green: the numbers are
1737
+ finite, the durations agree, nothing is degenerate. And `check` does not say *"your keys
1738
+ are offset"* โ€” it says the union box is a fifth larger than the reference's, the MAE is
1739
+ several times anything a wrong pose produces, and no slot is attributable anywhere.
1740
+ That reads like a **wrong rig**, so the hours go into the rig.
1741
+
1742
+ โ‡’ **Before reading a single measure, sample your own compiled animation and diff it
1743
+ against the pose series the fitter produced.** `sampleAnimation` in
1744
+ [`src/render.ts`](../src/render.ts) is the same stepper the frames were made with, so
1745
+ this is a handful of lines and it is exact: for every frame, for every bone, the local
1746
+ transform the file plays back against the local transform you fitted. A constant offset
1747
+ per channel is this bug; a constant *factor* is a unit or lever mistake; zeros
1748
+ everywhere are ยง9.1's `bone.pose` trap one level earlier. โญ **The general rule: a
1749
+ pipeline with a fit at one end and a file at the other needs one check that the file
1750
+ plays what the fit found**, and it belongs before the measures rather than after a day
1751
+ of them.
1752
+
1327
1753
  ### 9.2 Reading the table
1328
1754
 
1329
1755
  ```
@@ -1331,6 +1757,7 @@ cannot.
1331
1757
  reference 256x116px 0.117628 px/unit world x[-573.3 .. 1603.0] y[-81.2 .. 908.9] (frames.json)
1332
1758
  content candidate 234.6x95.5px at (11.3, 11.5) reference 234.7x95.3px at (11.2, 11.7) (union over 86 frame(s))
1333
1759
  โคท fit x0.999256 offset +0.05, -0.02 px rms 0.42 px over 344 edge(s) union residual -0.27 x +0.17 px aspect -0.30% (derived, 4 pass(es), settled)
1760
+ โญ MAE-refined by -1, +1 px: 54.31 โ†’ 48.47 over the reference's own pixels (10.7% of the figure). โ€ฆ
1334
1761
  in units candidate 1995.3 x 809.7 reference 1995.3 x 809.9 x0.9999
1335
1762
 
1336
1763
  โ”€โ”€ heavy โ€” candidate animation "heavy", 12 fps โ”€โ”€
@@ -1340,6 +1767,8 @@ cannot.
1340
1767
  slot drift worst 2.1 px "pendulum" at f0029
1341
1768
  per-frame 1 of 64 adjacent pair(s) change by a different amount than the reference does; worst
1342
1769
  f0018, yours moved 0 px where the reference moved 374
1770
+ sheet 311 of 311 tile(s) of contact.png at 64x57px in 8 column(s) MAE mean 4.30 worst 4.76 at f0047
1771
+ โคท worst 8: f0047=4.8 f0048=4.7 f0045=4.7 f0039=4.7 f0149=4.7 f0044=4.6 f0046=4.6 f0043=4.6
1343
1772
 
1344
1773
  the 9 frames worth reading โ€” worst by MAE, plus every frame whose own change disagrees, in index order
1345
1774
  frame MAE union px ฮ”px ref ฮ” worst slot drift how slots note
@@ -1401,6 +1830,12 @@ The lines, in order:
1401
1830
  does not, after the fit. This is the number that says *"something reaches
1402
1831
  somewhere nothing in the frames does, or is a different size"*, and a warning
1403
1832
  spells it out past a pixel.
1833
+ - the **MAE-refined** line, which is the last thing that happens to the box and the
1834
+ paragraph below is what it is for. On a **fitted** framing it says what constant
1835
+ whole-pixel offset was taken out and what that was worth (`โญ`), or that the
1836
+ search ran and the identity won. On a box that is not an estimate โ€” the frames'
1837
+ own, or one you pinned โ€” it never moves anything, and if it finds a constant there
1838
+ it says so as a **finding about your rig** rather than about the framing.
1404
1839
  - `in units` โ€” the same two boxes in world units. The framing absorbs a pure scale
1405
1840
  on purpose, so this is the only place one shows; it compares only if you measured
1406
1841
  the shot in the frames' own units.
@@ -1411,10 +1846,31 @@ that is a little large โ€” the best fit of the two extents is not quite the best
1411
1846
  alignment of the two pictures, and the fit spends a fraction of a pixel absorbing
1412
1847
  a difference that would have been cheaper to leave alone. Measured floor: about a
1413
1848
  third of a pixel on the ladder's shots. On most that is invisible; on a small
1414
- high-contrast frame it is worth a point or two of MAE โ€” rung 6 measured five. This
1415
- is the floor the frames' own box has no share in, which is why `check` prefers that
1416
- box whenever your pixels are measured to land in it; `--viewport` is how you stop
1417
- it in the cases that box does not cover.
1849
+ high-contrast frame it is worth a point or two of MAE โ€” rung 6 measured five, and
1850
+ on the spineboy sets a **constant** one or two pixels was worth 10โ€“30 % of the
1851
+ figure (issue #146). This is the floor the frames' own box has no share in, which is
1852
+ why `check` prefers that box whenever your pixels are measured to land in it;
1853
+ `--viewport` is how you stop it in the cases that box does not cover.
1854
+
1855
+ โญ **What a fitted framing now does about it: one final whole-pixel pass.** After
1856
+ the fit settles (or cycles), `check` searches every whole-pixel offset within ยฑ2 px
1857
+ for the lowest MAE over the reference's own drawn pixels and moves the box to the
1858
+ best one, when that is worth at least 1 % of the figure. So a fitted set's numbers
1859
+ are what is left **after** the best constant offset has been removed, rather than a
1860
+ constant offset read as motion โ€” and the line says which offset and what it bought,
1861
+ in both directions, so nothing is quietly absorbed.
1862
+
1863
+ Two things to know when you read it:
1864
+
1865
+ - โš ๏ธ **A large refinement on a set whose drift is also large is not necessarily
1866
+ framing.** The pass removes the best *constant*, and when one part carries much of
1867
+ the shot's ink a constant can absorb part of that part's own displacement. Read
1868
+ the offset beside the chain table: a big offset with a flat drift table is the
1869
+ fit's floor; a big offset with one limb far out is that limb.
1870
+ - On a box that is not an estimate the pass declines and says why. `frames.json`'s
1871
+ own box is where the frames were drawn, so a constant pixel *there* is your
1872
+ figure sitting a pixel off inside the right box โ€” a thing to fix, and the report
1873
+ refuses to frame it away. A pinned box is your claim, and nothing overrules it.
1418
1874
 
1419
1875
  **MAE** is the mean absolute RGB difference, 0..255, over the pixels either side
1420
1876
  covers โ€” the *union alpha*. It is not scored against a threshold, any more than a
@@ -1441,6 +1897,110 @@ much ink as the reference does gets `โš ๏ธ overdraw` beside those two numbers,
1441
1897
  both pixel counts, because at that point the first figure is cheap for a reason
1442
1898
  that has nothing to do with your keys.
1443
1899
 
1900
+ ๐Ÿšจ **Part of your MAE is the texture, not the animation, and nothing in the report
1901
+ says so.** The reference frames are rendered through the example's **own packed
1902
+ atlas**, and a packed atlas may carry a `scale:` line โ€” the ladder has one at
1903
+ `scale: 0.5`, whose 745x212 part is packed at 373x106. rigc has no packer (**ยง6**), so
1904
+ a candidate built from the loose PNGs samples a texture at twice that resolution and
1905
+ resamples every edge differently. The pixels are the same shape in the same place; they
1906
+ are filtered from a different source, and the difference lands on the outline of every
1907
+ part in every frame. It is a constant, it is invisible to `content`, `rms` and the
1908
+ `ยฑ2 px` refinement โ€” a resampling difference is not an offset โ€” and **no key you write
1909
+ can move it**.
1910
+
1911
+ โ‡’ **When the MAE is flat across the whole set and the drift is already at the floor,
1912
+ check the atlas before you look for keys.** The example's `.atlas` is an allowed input
1913
+ in its own right (`bench/runs/README.md`, *What a run may read*, item 4), and one line
1914
+ of it answers the question. To size the floor, re-run `check` once with
1915
+ `--atlas <the example's own .atlas>`: same skeleton, same keys, the reference's own
1916
+ texture. Measured on rung 3, MAE **6.13 / 6.01** with the candidate's own
1917
+ full-resolution atlas against **2.25 / 2.30** with the supplied one โ€” **two thirds of
1918
+ the figure was the texture**, and the run that did not know it would have spent its
1919
+ whole budget hunting a rig that was already right.
1920
+
1921
+ โš ๏ธ Two things about that diagnostic. It is a **diagnostic and not a better number**:
1922
+ the artifact `bench` validates ships its own atlas, so the first figure is the one
1923
+ that belongs in a run's record and the second is the explanation of where it went.
1924
+ And the coarser texture **loses** resolution the finer one has โ€” on the same rung a
1925
+ pair the reference moves *one pixel* across stopped being visible at half scale, so
1926
+ the diagnostic run reported a frame-change disagreement the graded run does not have.
1927
+ Read it for the floor, never as the verdict.
1928
+
1929
+ ๐Ÿšจ **And a third, which decides whether the recipe measures anything at all: `--atlas`
1930
+ substitutes region *geometry* as well as texture.** The diagnostic's logic is *"same
1931
+ skeleton, same keys, the reference's own texture"* โ€” but an atlas entry is not only a
1932
+ page and a rectangle. It also carries how the region was packed: **`rotate`**, and the
1933
+ trim offsets that say where the opaque part sits inside the original image. Swap the
1934
+ atlas and your attachments are re-seated on those, so the quads change too.
1935
+
1936
+ โ‡’ **The recipe measures a floor only when the substitution is *"same quads, coarser
1937
+ texture"*.** Where the supplied atlas packs its regions **rotated or trimmed** and your
1938
+ attachments were measured off the loose PNGs, it is not โ€” and the tell is unmistakable:
1939
+ **the number goes the wrong way.** A texture floor can only *explain* error, so a
1940
+ diagnostic that sends the MAE **up** on every set has substituted geometry, not just
1941
+ pixels, and the run's own atlas was the more faithful of the two.
1942
+
1943
+ โš ๏ธ **Then the honest verdict is *inconclusive*, not *no floor*.** Both readings stay
1944
+ open โ€” there may be a texture floor this diagnostic cannot isolate โ€” so record the
1945
+ figures, say the substitution changed the quads, and do **not** convert a failed
1946
+ diagnostic into a claim about the shot. โ‡’ Check the atlas's own entries for `rotate` and
1947
+ for trim before you run it; that is one look at a text file, and it tells you in advance
1948
+ whether the number you are about to take will mean anything.
1949
+
1950
+ ๐Ÿšจ **The precondition the advice above does not state: a floor measured with another
1951
+ part misplaced is not a floor.** Measuring at the rest pose is right โ€” it is the one
1952
+ pose you can often *prove*, because the setup pose is the art at its own scale and the
1953
+ frames state the standing dimensions โ€” but "the pose is provably right" is a claim
1954
+ about **one part**, and the floor you read is a whole-figure number. Any other part
1955
+ that can occlude the one you are measuring is inside that number too, and a part
1956
+ sitting tens of units off its place occludes the **wrong** pixels: the ones it hides
1957
+ count as yours-and-not-theirs, the ones it should have hidden count as
1958
+ theirs-and-not-yours, and both land on the part you thought you were isolating.
1959
+
1960
+ The damage is that you then hold a *plausible* floor and calibrate against it. A
1961
+ synthetic case with the same shape โ€” one part measured three ways, nothing about that
1962
+ part changed between the rows:
1963
+
1964
+ | what else is placed | silhouette IoU read for the measured part |
1965
+ | --- | --- |
1966
+ | a neighbour still tens of units out of place | 0.74 |
1967
+ | that neighbour placed | **0.95** |
1968
+ | (the difference) | 0.21, all of it the neighbour |
1969
+
1970
+ A fifth of an IoU is larger than most of what a fit is trying to buy, so two or three
1971
+ experiments get read against the wrong baseline before anything exposes it โ€” and what
1972
+ usually exposes it is the setup fit finishing, which is *after* you needed the number.
1973
+
1974
+ โ‡’ **Before believing a floor, check that every part which can occlude the one you are
1975
+ measuring is already placed** โ€” and prefer a frame where the parts are **far apart or
1976
+ only one is drawn** to one where they overlap, which is ยง8.1's rule for calibrating a
1977
+ two-part assignment applied to a floor. If no such frame exists, say in the log that
1978
+ the floor is an upper bound on the error rather than a floor under it. โš ๏ธ This is the
1979
+ same failure as capturing a guard's expected value from a screen that is already
1980
+ broken: the baseline records the defect, and then the *repair* is what looks wrong.
1981
+
1982
+ โš–๏ธ **`frames.json`'s own box can be refused for a reason that is not a coordinate
1983
+ error, and there is an honest answer.** The test is on **extent**: a candidate authored
1984
+ in the frames' own world units โ€” one whose setup box lands on the reference's to the
1985
+ pixel โ€” still fails it if its union content box differs by a few pixels at the
1986
+ extremes, because one part reaching somewhere nothing in the frames reaches is enough.
1987
+ `check` then fits its own box, and on a multi-shot root the fitted framing costs every
1988
+ set some MAE against the declared one โ€” the same order as the shared-versus-per-set gap
1989
+ the `--framing` flag's own help quotes, and easily more than a round of fitting buys.
1990
+ That cost is real and it is **not** a sign you got the coordinates wrong.
1991
+
1992
+ โ‡’ **Report both, label which is which, and say what separates them.** Run `check`
1993
+ unaided โ€” that is the figure the artifact produces on its own and the one that belongs
1994
+ in a run's record โ€” then run it once more with `--viewport` on the declared box and keep
1995
+ that output as a **named diagnostic file** beside the first. The gap between them is the
1996
+ framing; what is left is the keys, which is the only reason to want the second number.
1997
+ ๐Ÿšซ **The pinned run is never the record.** `--viewport` is a claim about your own
1998
+ coordinates and `check` says so above every figure it prints under one: *nothing checks
1999
+ it*. And do not chase the refusal by shrinking a part to fit the box โ€” that trades a
2000
+ framing cost for a wrong silhouette, which is worse in every column that matters.
2001
+ Instead read the `content` line's own advice: it names how much wider and shorter you
2002
+ cover, and **which part reaches too far is a drift question**, not a framing one.
2003
+
1444
2004
  **`ฮ”px` and `ref ฮ”`** are the two columns that do **not** compare you against the
1445
2005
  reference. They compare each side against **itself one frame earlier**: how many
1446
2006
  pixels of your own frame moved since your own previous frame, and the same for the
@@ -1466,12 +2026,46 @@ line whenever the MAE is flat and something still looks wrong: a flat MAE says t
1466
2026
  framing and the art agree, and it says nothing at all about whether your shot holds
1467
2027
  and blinks where the reference does.
1468
2028
 
2029
+ โš ๏ธ **Each set is compared against ITSELF, so on a shot committed at two rates a hold
2030
+ can exist in one set and not the other.** The coarse set samples every other frame of
2031
+ the fine one, so a pair the coarse set holds across is a constraint between samples
2032
+ **2k and 2k+2** of the fine series โ€” and 2k+1 between them is free to move, and does.
2033
+ One ladder shot has a pair whose whole-frame change at the coarse rate is **exactly
2034
+ 0** while both fine-rate pairs inside that same span change by 48 px: the subject
2035
+ shifts under three world units and comes back, so the two coarse samples land on the
2036
+ same pose either side of it. So ยง10.3's *key both ends of the hold* has to be applied
2037
+ **at each committed rate separately**, and equal values are not enough โ€” the two
2038
+ samples have to be **keys**, or a planner reduces through one of them and an
2039
+ interpolant inside its own tolerance is not equality. That cost two builds on that
2040
+ run, the second of them for exactly that reason. โ‡’ Measure the frames' own
2041
+ frame-to-frame change **once per committed rate**, and where a rate holds, pin both
2042
+ ends as keys whenever a finer rate moves between them.
2043
+
1469
2044
  โš ๏ธ Only between **adjacent** frames. A set that ships stills rather than every frame
1470
2045
  โ€” rung 2's contact-sheet sets โ€” reports `no two compared frames are adjacent`, and
1471
2046
  means it: the difference between two frames 310 apart is not a frame-to-frame delta.
1472
2047
  A disagreement needs one side to hold *exactly* still while the other moves, or one
1473
2048
  side to move four times the other and at least two dozen pixels more; below that the
1474
- two rasterisations differ by their own last bit and the column says nothing.
2049
+ two rasterisations differ by their own last bit and the column says nothing. Such a
2050
+ set gets the `sheet` line instead, which is MAE over every sampled frame and not a
2051
+ change measure โ€” the two thresholds above are pixel counts at frame scale, and a
2052
+ tile has a fraction of a frame's pixels.
2053
+
2054
+ **The `sheet` line is the whole shot**, on the sets that commit a couple of stills
2055
+ and fold every sampled frame into one `contact.png`. It says how many tiles were
2056
+ compared out of how many the sheet holds, the grid it measured off the sheet itself,
2057
+ the mean and worst tile, and the worst eight by MAE. Read the **series** rather than
2058
+ the mean, exactly as with the frame table: flat across the shot is framing or art,
2059
+ a spike is timing at that moment โ€” rung 2's four shots read 4.30โ€“4.41 flat over
2060
+ 1,244 tiles, which is what says their trajectories, ring rates and attachment swaps
2061
+ land where and when they should. Two things to know:
2062
+
2063
+ - it is measured in the **same box** the frame table was, at the sheet's scale. For
2064
+ a stills-plus-sheet set that box was decided on the stills, so a set whose framing
2065
+ is a fit carries that fit into these numbers as well;
2066
+ - a sheet whose dimensions are not a grid of this set's frame count at these frames'
2067
+ aspect is **refused by name** rather than read wrong โ€” the note names the file, and
2068
+ the answer is to re-render the set.
1475
2069
 
1476
2070
  **Slot drift** is what you act on. For each of your slots, `check` measures where
1477
2071
  it landed and how far that is from where the reference put it. That names the part,
@@ -1481,8 +2075,15 @@ sentence you can take straight back to a key.
1481
2075
  There are two matchers and the `how` column says which one answered:
1482
2076
 
1483
2077
  - `component` โ€” your slot sits on a connected component of the reference frame that
1484
- is its own size. The drift is the distance between the two centroids, and it is
1485
- the strongest answer available.
2078
+ is its own size **and holds nothing else you drew**. The drift is the distance
2079
+ between the two centroids, and it is the strongest answer available. All three
2080
+ conditions are checked: a blob may not be much bigger than the slot, may not be
2081
+ much wider than its box, and may not contain another of your parts' ink. The last
2082
+ is the one a dominant part slips through otherwise โ€” rung 2's course is 81 % of a
2083
+ blob that also holds the water, the panel and both rings, so the blob is only
2084
+ 1.24x its ink and no wider than its box, and the reported *"course drift 11.2 px"*
2085
+ was the distance to a five-part centroid (issue #37). It now falls to the matcher
2086
+ below and reads 0.0 px.
1486
2087
  - `tmpl 0.62` โ€” the reference merged your slot into a neighbour (they touch, or one
1487
2088
  is drawn over the other), so the fallback rendered **your slot on its own** and
1488
2089
  correlated it against the reference around where you drew it. The number is the
@@ -1508,19 +2109,19 @@ a part you have not authored, or one you have put somewhere else entirely.
1508
2109
  ```
1509
2110
  chains 6 from the candidate's own bone tree โ€” the roster is at the foot of the report
1510
2111
  chain slots worst slot drift mean MAE in it share
1511
- neck 5/5 3.0 px "goggles" f0004 2.3 px 30.37 24.3%
1512
- rear-upper-arm 6/6 2.2 px "gun" f0000 1.4 px 46.26 34.6%
1513
- front-thigh 3/3 16.3 px "front-shin" f0003 7.2 px 48.18 9.0%
2112
+ crest 5/5 3.0 px "lantern" f0006 2.4 px 28.40 31.5%
2113
+ prop-arm 6/6 2.0 px "prop" f0002 1.5 px 44.90 33.0%
2114
+ near-strut 3/3 15.0 px "strut-tip" f0004 7.0 px 51.30 8.5%
1514
2115
  ```
1515
2116
 
1516
2117
  `check` cuts **your own** bone tree at every branch point โ€” a chain runs from a
1517
2118
  root or a fork down to the next fork; a single-bone chain that is itself a fork
1518
- folds into its parent, so a `torso` that branches three ways joins the trunk rather
2119
+ folds into its parent, so a `hub` that branches three ways joins the trunk rather
1519
2120
  than becoming a row of its own; and each chain is named after the first bone in it
1520
- that carries a slot. On a biped that lands on the parts you would name (`neck`,
1521
- `front-thigh`, the gun arm); on a serial figure with no fork it is one chain, and
1522
- the slot rows under it still say which link moved. Which bones and slots went where
1523
- is printed as a roster at the foot of the report, so it is never a guess. `MAE
2121
+ that carries a slot. On a biped that lands on the parts you would name (`crest`,
2122
+ `near-strut` and `prop-arm` above); on a serial figure with no fork it is one chain,
2123
+ and the slot rows under it still say which link moved. Which bones and slots went
2124
+ where is printed as a roster at the foot of the report, so it is never a guess. `MAE
1524
2125
  share` divides the difference over the **reference's** own drawn pixels โ€” the
1525
2126
  denominator from the line above, which nothing you draw can grow โ€” and splits it by
1526
2127
  giving each of those pixels to the chain whose ink is nearest, so the shares
@@ -1536,6 +2137,19 @@ a per-shot list. **ยง8.1** is how to act on it: the next iteration goes to the w
1536
2137
  chain by error per pixel, and a chain already at the floor is frozen rather than
1537
2138
  re-fitted.
1538
2139
 
2140
+ โš ๏ธ **One exception to "0 % is the loudest row", and on a mesh rig it is the common
2141
+ case: a chain whose roster reads `(draws nothing)` rather than `0/n`.** Those are two
2142
+ different states and the table prints them differently. `0 %` on `0/3` **slots drawn**
2143
+ means three slots exist on that chain and none of them put ink on the frame โ€” that is
2144
+ the loud row, and it is a missing part. `(draws nothing)` in the **bones** roster means
2145
+ the chain carries **no slot at all**, and a mesh's control bones are exactly that: the
2146
+ mesh attachment lives on the slot of the bone the mesh hangs from, so the bones that
2147
+ *deform* it own nothing to draw. โ‡’ **On a mesh rig that row is normal and quiet.** Read
2148
+ the roster at the foot of the report before reacting to a chain's share: if the chain's
2149
+ slots column is a parenthesis rather than a fraction, the deformation it carries is
2150
+ already being scored inside the chain that owns the slot, and the row is telling you
2151
+ about your bone tree rather than about a hole in your figure.
2152
+
1539
2153
  ### 9.3 What it still cannot see
1540
2154
 
1541
2155
  - **Anything a frame does not contain.** Bone `length`, the setup `inherit` mode,
@@ -1566,10 +2180,22 @@ re-fitted.
1566
2180
  3 px low at one frame have the same drift and opposite causes. The table gives
1567
2181
  you the frame index; ยง8's rule still applies โ€” look for a second way to get the
1568
2182
  number before you author the key.
1569
- - **What happens between two committed frames.** `ฮ”px` compares adjacent frames and
1570
- a set that ships stills has none, so a shot that is right at every committed frame
1571
- and wrong between them reads clean. That is the same gap `--frames` on a
1572
- contact-sheet set already has, and it is why the frame-count line is printed.
2183
+ - **What happens between two committed frames** โ€” *unless the set ships a sheet.*
2184
+ `ฮ”px` compares adjacent frames and a set that ships stills has none, so a shot
2185
+ that is right at every committed frame and wrong between them reads clean in that
2186
+ column. That is the same gap `--frames` on a contact-sheet set already has, and it
2187
+ is why the frame-count line is printed. โญ What closes it is the **`sheet` line**:
2188
+ on a set rendered at a higher rate than the frames on disk, every sampled frame is
2189
+ compared, so the samples between two committed ones are measured there and
2190
+ nowhere else.
2191
+ ๐Ÿšจ **And when you go looking for one of them, do not assume it lies between its
2192
+ neighbours.** A half-frame is not an interpolation โ€” it is where the shot actually
2193
+ was, and the interesting ones are interesting because it left the interval. Rung
2194
+ 4's ball makes contact entirely inside one twelfth of a second: the sheet puts it
2195
+ **40 px below both 12 fps frames that bracket it**, while its x sits within their
2196
+ own range. A search whose reach was scaled from those two frames' own step
2197
+ therefore looked ยฑ9 px for something 40 px away and reported the interpolation
2198
+ back, twice, before the tile was read rather than inferred.
1573
2199
 
1574
2200
  ---
1575
2201
 
@@ -1714,6 +2340,146 @@ that is not held"* from the other side, and the same place catches it โ€” a slop
1714
2340
  hold shows up in `check`'s per-frame column and nowhere else, because it is cheap in
1715
2341
  every single frame and wrong only in the relation between two.
1716
2342
 
2343
+ โš ๏ธ **The key reducer has to key the plateau, because a plateau is neither an end nor
2344
+ a turn.** A planner that forces the series ends and every change of direction โ€”
2345
+ which is exactly what the rule above asks for โ€” will still author a slope straight
2346
+ through a run of still frames: not one sample in the middle of a hold is an end or
2347
+ a turning point, and a greedy span stays inside its own per-bone tolerance the whole
2348
+ way across. **A tolerance is not a hold.** Slow motion inside the tolerance is a
2349
+ tolerance question; stillness is a thing the shot *does*, and it survives the
2350
+ reduction only if the reduction is told to keep it. โ‡’ Force **both ends of every run
2351
+ of equal values** as keys in their own right โ€” a third kind of forced index beside
2352
+ the series ends and the turning points โ€” and test on **exact** equality, so that a
2353
+ merely near-still span is deliberately not swept up with it. This is worth doing
2354
+ before you have any evidence you need it: a run whose *poses* are all right can fail
2355
+ this and see nothing wrong anywhere else, because `validate` has no opinion on it,
2356
+ `diff` never looks at a rendered frame, and an aggregate MAE is cheap in every
2357
+ single frame and wrong only in the relation between two.
2358
+
2359
+ โš ๏ธ **And a tolerance is not a *slow span* either โ€” a key tolerance needs a relative
2360
+ floor beside its absolute one.** The rule above rescues exact stillness by forcing it,
2361
+ and deliberately leaves near-stillness to the tolerance. But **ยง9.2's per-frame column
2362
+ compares *changes*, not positions**, so on a span the shot barely moves across, an
2363
+ error well inside an absolute tolerance is most of the motion. Measured, on rung 3: the
2364
+ reference moves **0.109 px** between two frames; a greedy span deviated **0.098 px**
2365
+ there, legal under a 0.30 px tolerance and legal again under 0.15 px โ€” and the column
2366
+ read **259 px against the reference's 40**, a six-fold disagreement authored by a span
2367
+ that was, at every keyframe, exactly right. The same tolerance that is generous on the
2368
+ fast part of the shot is a 90 % error on the slow part, because one figure in pixels
2369
+ cannot be both.
2370
+
2371
+ โ‡’ Cap each span's deviation at **the smaller of the absolute tolerance and the
2372
+ smallest single-frame move inside that span**. It is one line in the planner, it costs
2373
+ a handful of keys, and it is the difference between a reduction that is accurate and
2374
+ one that is accurate *in proportion to what is happening*.
2375
+
2376
+ โš ๏ธ **The opposite defect exists and forcing keys makes it worse.** Everything above is
2377
+ one direction โ€” *my curve slopes through a plateau the reference holds* โ€” and its fix is
2378
+ to force both ends as keys. The other direction is *my candidate moves several times
2379
+ what the reference does on a pair the reference barely moves across*, and if you reach
2380
+ for the same fix you will pin the excess in place instead of removing it. **The cause is
2381
+ different**: there the key plan was smoothing away motion the shot has; here the key
2382
+ plan is faithful and what disagrees is the **per-frame residual** โ€” two neighbouring
2383
+ poses each a little off, in opposite directions, so the *difference* between them is
2384
+ several times either error. Forcing both as keys asks the planner to reproduce exactly
2385
+ the two poses whose disagreement is the problem.
2386
+
2387
+ โญ **Diagnose it before you fix it, with one comparison.** Take the two frames the column
2388
+ flags and ask whether your **poses** at those two frames are each inside your own fitting
2389
+ accuracy. If they are โ€” and the pair still disagrees โ€” the defect is the residual and not
2390
+ the plan. Synthetic case, one pair:
2391
+
2392
+ | | reference moves | candidate moves | each pose's own error |
2393
+ | --- | --- | --- | --- |
2394
+ | a quiet pair | 0.8 px | 4.1 px | 1.6 px and 1.7 px, opposite signs |
2395
+
2396
+ Both poses are ordinary; the pair is a five-fold disagreement built out of them.
2397
+
2398
+ โ‡’ **The fix has the same shape as the relative floor above: make the smoothing slack
2399
+ relative to the reference's own local change.** Where the reference barely moves,
2400
+ contract your neighbouring poses toward each other until your own frame-to-frame change
2401
+ is inside the band โ€” accepting a small, *bounded* loss of fidelity on those frames in
2402
+ exchange for the one measure that can see a hold. โš ๏ธ **That is a trade and it is recorded
2403
+ as a trade**: name the frames, name the cost per frame, and say in the log that you took
2404
+ it. A contraction reported as a fit is the same dishonesty as a hold reported as a
2405
+ measurement, and the cost is real โ€” the frames you contracted are slightly less faithful
2406
+ than they were.
2407
+
2408
+ ๐Ÿšจ **Contract the *planned curves*, not the pose series โ€” the report never sees the pose
2409
+ series.** This is one sentence and it is worth two builds: the change column measures
2410
+ your **compiled animation sampled at the frames' own rate**, and between your poses and
2411
+ that lie the key reduction and the curves. Contract before the reduction and you have
2412
+ adjusted a series nothing downstream reads โ€” the planner then re-fits its spans through
2413
+ the adjusted poses, the interpolants land where the tolerance allows, and the pair you
2414
+ were aiming at comes back out of band having *moved*. โ‡’ **Apply the contraction where
2415
+ the measurement is taken**: plan the keys, sample the planned curves, find the offending
2416
+ pairs, and contract *those samples* by forcing or moving the keys that produce them โ€”
2417
+ then re-plan and re-sample. That is the closing loop below, and its subject is the curve
2418
+ series throughout.
2419
+
2420
+ โš ๏ธ **And aim *inside* the band, not at it.** `check`'s thresholds are exact and stated in
2421
+ [`src/check.ts`](../src/check.ts), so it is tempting to converge until every pair is
2422
+ just inside. But a run measuring its own change renders in **its own framing**, and the
2423
+ report renders in the one `check` chose โ€” and a fraction of a percent of scale is worth
2424
+ a few percent of a pixel count. A pair you cleared by a hair in your loop can sit the
2425
+ wrong side of the same threshold in the report, on a difference that is entirely
2426
+ framing. โ‡’ Converge to a **margin** โ€” clear the band by enough that a percent of scale
2427
+ cannot cross it โ€” and re-read the real report before believing the column.
2428
+
2429
+ ๐Ÿšจ **There is a third direction, and on a busy shot it is the binding one: your candidate
2430
+ moving too *little*.** The two cases above are both *you moved when you should not have*
2431
+ โ€” a hold that is not held, and excess change on a quiet pair. But `check`'s rule is
2432
+ **two-sided**: it faults a pair when **either** side moves several times the other by
2433
+ more than its pixel floor. So the mirror case is a reference that is genuinely busy and
2434
+ a candidate that reproduces a fraction of it, and nothing in the two paragraphs above
2435
+ names it.
2436
+
2437
+ โญ **The practical form is a floor rather than a ceiling: on every pair the reference
2438
+ moves, yours has to move at least about a quarter as much.** Read the exact multiple and
2439
+ the pixel floor out of [`src/check.ts`](../src/check.ts) rather than trusting the
2440
+ approximation โ€” but plan against the floor, because it behaves quite differently from
2441
+ the ceiling:
2442
+
2443
+ - **It is not fixed by keys.** Over-change is a planning artefact you can force or
2444
+ contract away. Under-change means the *poses themselves* barely differ, so no key plan
2445
+ recovers it โ€” the fit has to find more motion before the planner sees any.
2446
+ - **It is where a whole-figure objective fails hardest**, which is why the item below
2447
+ belongs beside it: a passage whose motion is a small part against a large still body
2448
+ contributes almost nothing to a whole-shot score, so a fitter converges happily on a
2449
+ near-static series and every pose looks fine on its own.
2450
+ - โš ๏ธ **And the band will accept a shot that is visibly underplayed.** Clearing the floor
2451
+ at a quarter is not reproducing the motion; it is not *failing* it. A run whose busy
2452
+ passage sits near the floor should say so in the log as a known-weak passage rather than
2453
+ quote the column as if it were a fidelity result โ€” the column is a **band**, and a band
2454
+ is the widest thing that passes, not the thing you were aiming at.
2455
+
2456
+ โญ **Then stop trusting the floor and close the loop on the frames, because a floor is
2457
+ a heuristic and the column is a measurement.** The floor above cut rung 3's
2458
+ disagreements from three to one and could not reach the last: **sample your own planned
2459
+ curves at the frames' own rate, render them, compare every adjacent pair against the
2460
+ reference's own change, force the offending frames as keys, and re-plan** โ€” repeating
2461
+ until no pair is out of band. It terminates quickly (that shot needed one extra round
2462
+ on one animation and none on the other), it needs no build, and it is the only part of
2463
+ key planning that is verified rather than argued. โš ๏ธ The band is worth reading before
2464
+ you aim at it: `check` calls a pair a disagreement when one side is **exactly** still
2465
+ and the other is not, or when one side moves **four times** the other **and** at least
2466
+ **24 px** more (`src/check.ts`). So the wide middle of a shot is nearly free and the
2467
+ whole difficulty is the pairs where the reference barely moves โ€” including, on that
2468
+ rung, one pair it moves a single pixel across, which no MAE and no drift figure in this
2469
+ toolchain can see.
2470
+
2471
+ ๐Ÿšจ **But check that the shot holds at all first, because applying this to one that
2472
+ never does manufactures the defect it prevents.** The rule is about a *shot*, and a
2473
+ snap-to-still step in a planner does not cost nothing when there is nothing to snap:
2474
+ it will find some run of samples inside the fitter's own resolution and flatten it.
2475
+ Rung 4's shot has **no** still span โ€” not one adjacent pair of its 155 reference
2476
+ frames is pixel-identical, and even its last two differ, because the chain is still
2477
+ settling at the final frame โ€” and the snap duly flattened the tail and put a
2478
+ *"yours moved 0 px where the reference moved 28"* into the per-frame column: ยง9.2's
2479
+ held-pose defect arriving from the opposite direction. โ‡’ Difference every adjacent
2480
+ pair of frames once, before the planner runs. It is one pass over the set, and it
2481
+ tells you whether this paragraph applies to you at all.
2482
+
1717
2483
  ๐Ÿ“— **Add a key when a curve cannot carry the shape.** *"If a curve is not smooth
1718
2484
  enough, it is easily remedied by adding another key"*, and the **Bounce** handle
1719
2485
  preset exists for *"changing directions abruptly, such as when a ball bounces"* โ€”
@@ -1746,6 +2512,87 @@ by that bone's lever arm. With that, the whole trade reads as one curve and you
1746
2512
  pick a point on it deliberately: that shot measured 0.6 px โ†’ 259 keys โ†’ 1.619 window
1747
2513
  MAE, 0.3 px โ†’ 300 โ†’ 1.402, 0.15 px โ†’ 377 โ†’ 1.305.
1748
2514
 
2515
+ โš ๏ธ **Compute what a skipped sample costs before you declare that tolerance โ€” on a
2516
+ fast subject the shot's own speed has already pinned it.** Skipping one sample means
2517
+ spanning it linearly, and the chord through its two neighbours sits at their mean, so
2518
+ the deviation at the sample skipped is **half** the series' second difference there:
2519
+ `|f(nโˆ’1) โˆ’ 2ยทf(n) + f(n+1)| / 2`. That is an identity, not an approximation โ€” check
2520
+ it on `f(n) = nยฒ`, whose second difference is 2: the chord reads `nยฒ + 1` where the
2521
+ curve reads `nยฒ`, and the deviation is 1. So second-difference the fitted series,
2522
+ halve it, and read that number against the tolerance you were about to declare. If it
2523
+ is the larger of the two, the fitter keys nearly every frame, and no tolerance below
2524
+ it changes that โ€” the key density is then a fact about the subject, not a choice you
2525
+ made. One ladder run is that case: its subject's median frame-to-frame second
2526
+ difference measured 6.4 px, so a span that skips one sample deviates about 3.2 px,
2527
+ ten times the 0.3 px tolerance the run declared, and the tolerance would have to be
2528
+ loosened past that 3.2 px before a span could afford to skip anything at all. What
2529
+ the trade bought there was accuracy and never sparsity โ€” 0.6 px โ†’ 439 keys โ†’ 18.22
2530
+ MAE at 12 fps, 0.45 px โ†’ 482 โ†’ 17.54, 0.3 px โ†’ 521 โ†’ 17.26. โ‡’ Do the arithmetic
2531
+ first. It tells you which of the two situations you are in: picking a point on the
2532
+ curve above, or discovering the point the shot has already put you on โ€” and the
2533
+ second one is not a failure to reach the density this section asks for, it is what
2534
+ that density is here.
2535
+
2536
+ ๐Ÿšจ **There is a third situation, and it is the one where nothing in the loop can
2537
+ help you: a tolerance under the accuracy of whatever produced the series.** The
2538
+ arithmetic above is about the *subject*; this is about the *estimator*. A fitted
2539
+ series is only as good as its objective's basin, and a tolerance below that width
2540
+ buys keys that encode the fitter's wander โ€” and **`check` cannot see that it
2541
+ happened**, because two candidates that are both inside tolerance render the same
2542
+ pixels. `diff`'s `key_counts` sees it and `diff` is the finish line, so a run gets
2543
+ one shot at the number. Rung 4's is the recorded case: it declared 0.28 px, and its
2544
+ own objective's basin on the shortest lever in the chain measured **ยฑ1.5ยฐ**, which
2545
+ is ยฑ0.5 px there โ€” so the declared figure was under the noise, and it shipped about
2546
+ three times the reference's key count (`key_counts` 421/1339) with every other
2547
+ animation measure at or near 1.000. โ‡’ **Measure the basin before you declare the
2548
+ tolerance**, which costs nothing and needs no reference: scan each knob around its
2549
+ converged value and read how far it moves before the objective does. Then declare a
2550
+ tolerance at or above the widest of them, and record both numbers.
2551
+
2552
+ โš–๏ธ **Read that last sentence with the rule two paragraphs up, because taken literally
2553
+ the two pull apart โ€” and the resolution is that the basin is a *per-channel floor*,
2554
+ not a second global declaration.** The tension is real: *declare one tolerance* asks
2555
+ for a single figure in one unit so the density trade reads as one curve, while
2556
+ *declare at or above the widest basin* points at the worst-identified knob in the rig.
2557
+ Those can differ by **an order of magnitude** โ€” a well-levered channel the objective
2558
+ pins to a fraction of a pixel sitting in the same rig as a part the objective barely
2559
+ sees at all, whose basin is several pixels wide. Take the widest and every good channel
2560
+ is keyed to the worst one's ignorance; take the declared figure alone and the bad
2561
+ channel ships the fitter's wander as data.
2562
+
2563
+ โญ **What decides it: a basin belongs to the estimator that wrote the channel, and a
2564
+ run that fits poses has more than one.** The two rules are answering different
2565
+ questions. *One tolerance* is about the **unit and comparability** of the figure you
2566
+ declare โ€” that survives untouched. *The basin* is about the **noise under a particular
2567
+ series**, and noise is a property of the estimator on that channel, not of the rig. So:
2568
+
2569
+ > **Declare one tolerance, in pixels at the end of what each bone swings. Then floor it
2570
+ > per channel at that channel's own basin, capped.** Effective tolerance for a channel
2571
+ > = `max(declared, min(that channel's basin, cap))`.
2572
+
2573
+ - **Why per channel** โ€” the thing the floor protects against is encoding wander, and
2574
+ wander is per channel. A global maximum spends keys nowhere they were needed and
2575
+ removes them nowhere they were wrong.
2576
+ - **Why a cap, and this is the part worth understanding.** The basin bounds **what you
2577
+ know**; the tolerance also bounds **what you render**, and the rendered series is read
2578
+ by `check`'s change column at *zero* slack (ยง9.2). So an uncapped floor lets a
2579
+ badly-identified channel buy a reduction error large enough to show up as motion the
2580
+ reference does not have โ€” trading a measure nothing reads for one read at zero
2581
+ tolerance, which is the wrong direction. A cap of a pixel or two, declared and
2582
+ recorded, bounds the reduction error whatever the identifiability.
2583
+ - โ‡’ **And a channel whose basin exceeds the cap is telling you it is not identified,
2584
+ which is a different problem with a different fix.** The answer there is a **prior** โ€”
2585
+ regularise the channel toward a smooth trend and say in the log that you did โ€” not a
2586
+ tolerance wide enough to key it three times and call the result a measurement. โš ๏ธ Such
2587
+ a channel is *partly a prior rather than a measurement*, and a run that does this
2588
+ records which channels and over which passages, exactly as it records a contraction
2589
+ trade below.
2590
+
2591
+ ๐Ÿ“Œ **Record all three numbers**: the declared tolerance, each floored channel's basin,
2592
+ and the cap. `diff`'s `key_counts` is the finish line and a run gets one shot at it, so
2593
+ the arithmetic that produced the density is the only thing that makes the figure
2594
+ readable afterwards.
2595
+
1749
2596
  **A rig's parameters are not identified by its pixels โ€” remove the gauges before you
1750
2597
  key.** A bone that carries no attachment is an exact gauge: turn it by ฮด, turn its
1751
2598
  children back by ฮด, and **not one pixel changes**. Anything optimising against pixels