spine-rigc 0.4.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/README.md CHANGED
@@ -198,11 +198,25 @@ exit code either command could produce.
198
198
 
199
199
  ### Benchmark ladder โ€” the rungs, and where they stand
200
200
 
201
+ ๐ŸŽ“ **The ladder is complete, 2026-08-28.** All eight numbered rungs and the
202
+ spineboy graduation exam are cleared under gate v2.1, every clause PASS or SKIP:
203
+ worst attributable slot drift **5.55 px** against a 6.0 px bar, and **0 of 124**
204
+ frame-change disagreements. Recompiling the same spec in a different session
205
+ reproduced every field of the measurement record **to the digit**. The rungs stay
206
+ in place as regression gates.
207
+
208
+ โš ๏ธ **What that certifies, stated exactly.** That **the tool, the guide and the
209
+ protocol reach the bar across a bounded series of honest attempts, each residual
210
+ diagnosed and fixed** โ€” spineboy took five, and the last inherited its
211
+ predecessor's specs under the run protocol's inheritance clause. It is **not**
212
+ that an agent authors a spineboy-scale rig from the brief alone in one run: the
213
+ ladder has not demonstrated that, and each row records which of the two it is.
214
+
201
215
  **[docs/LADDER.md](docs/LADDER.md) is the live ledger**: the rung order
202
216
  (blockers โ†’ rung 3 first โ†’ 1 ยท 2 ยท 4 ยท 5 โ†’ 6 โ†’ 8 โ†’ 7 โ†’ spineboy), what each
203
217
  rung gates on, how a rung is scored, the honesty rule that keeps the reference
204
218
  export away from the authoring agent, the operating rules โ€” what a pass is, and
205
- the numbered thresholds of the current gate (**gate v2**) that decide one โ€” and a status table. Run
219
+ the numbered thresholds of the current gate (**gate v2.1**) that decide one โ€” and a status table. Run
206
220
  one with:
207
221
 
208
222
  ```bash
package/docs/AUTHORING.md CHANGED
@@ -1022,6 +1022,34 @@ gone by construction, and the reading needs no knowledge of which parts are invo
1022
1022
  beside the figure too, because an edge the frames really decide wins shot after shot
1023
1023
  rather than on a couple of them.
1024
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
+
1025
1053
  **Calibrate the band with a control on an edge the brief has already settled by
1026
1054
  measurement.** Run the same test on that edge, read how far apart the two builds
1027
1055
  come out over the pixels that decide it, and treat that separation as the scale a
@@ -1210,6 +1238,26 @@ of the chain in question**, never a whole foreign pose: a foreign pose puts the
1210
1238
  where this shot never goes, and the rest of the search then spends itself fighting
1211
1239
  what the borrow brought with it.
1212
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
+
1213
1261
  **Re-fit the setup pose against frames drawn from every shot, not against one.** Every
1214
1262
  animation is measured from the setup pose, so an error in it is an error in all of
1215
1263
  them โ€” and it is exactly the error one frame cannot show you. Fit an attachment's
@@ -1222,6 +1270,47 @@ against a handful of frames drawn from **every** animation at once, and hold it
1222
1270
  while the per-frame poses are fitted. It is the spread that identifies it โ€” a
1223
1271
  sequence of single-frame fits, one per shot, is not the same thing.
1224
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
+
1225
1314
  **Seed each frame's search from its neighbour's solution โ€” as one start among the
1226
1315
  full-range scans, never instead of them.** Adjacent frames are adjacent poses, so the
1227
1316
  answer next door is a better first guess than the middle of any range, and it costs one
@@ -1276,6 +1365,23 @@ not a quiet one.** The matcher refuses to name a distance past the part's own si
1276
1365
  (ยง9.2), so a limb far enough out reports no match rather than a large number โ€” read
1277
1366
  that beside a high figure per pixel as the strongest signal the table has.
1278
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
+
1279
1385
  **What comes out is a pose per frame, and a pose per frame is not a key.** Two things
1280
1386
  decide what survives the reduction, and **ยง10.3** states both: declare one tolerance
1281
1387
  in pixels at the end of what each bone swings rather than a figure in degrees, and
@@ -1319,6 +1425,27 @@ tile has a fraction of a frame's pixels, so the per-frame change measure stays o
1319
1425
  the committed stills, where it reports `no two compared frames are adjacent` and
1320
1426
  means it.
1321
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.
1448
+
1322
1449
  `--fps <n>` exists for frame sets that have no `frames.json` beside them, which are
1323
1450
  sets rendered before the sidecar existed: it gives the rate those frames were
1324
1451
  sampled at, and without it the 12 fps protocol rate is assumed and the report says
@@ -1484,6 +1611,145 @@ of these inert writes and not a wrong animation.** The parameter you swept was
1484
1611
  never read; a wrong rig moves the number, a write to a field nothing reads
1485
1612
  cannot.
1486
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
+
1487
1753
  ### 9.2 Reading the table
1488
1754
 
1489
1755
  ```
@@ -1660,6 +1926,81 @@ pair the reference moves *one pixel* across stopped being visible at half scale,
1660
1926
  the diagnostic run reported a frame-change disagreement the graded run does not have.
1661
1927
  Read it for the floor, never as the verdict.
1662
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
+
1663
2004
  **`ฮ”px` and `ref ฮ”`** are the two columns that do **not** compare you against the
1664
2005
  reference. They compare each side against **itself one frame earlier**: how many
1665
2006
  pixels of your own frame moved since your own previous frame, and the same for the
@@ -1796,6 +2137,19 @@ a per-shot list. **ยง8.1** is how to act on it: the next iteration goes to the w
1796
2137
  chain by error per pixel, and a chain already at the floor is frozen rather than
1797
2138
  re-fitted.
1798
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
+
1799
2153
  ### 9.3 What it still cannot see
1800
2154
 
1801
2155
  - **Anything a frame does not contain.** Bone `length`, the setup `inherit` mode,
@@ -2019,6 +2373,86 @@ smallest single-frame move inside that span**. It is one line in the planner, it
2019
2373
  a handful of keys, and it is the difference between a reduction that is accurate and
2020
2374
  one that is accurate *in proportion to what is happening*.
2021
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
+
2022
2456
  โญ **Then stop trusting the floor and close the loop on the frames, because a floor is
2023
2457
  a heuristic and the column is a measurement.** The floor above cut rung 3's
2024
2458
  disagreements from three to one and could not reach the last: **sample your own planned
@@ -2115,6 +2549,50 @@ tolerance**, which costs nothing and needs no reference: scan each knob around i
2115
2549
  converged value and read how far it moves before the objective does. Then declare a
2116
2550
  tolerance at or above the widest of them, and record both numbers.
2117
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
+
2118
2596
  **A rig's parameters are not identified by its pixels โ€” remove the gauges before you
2119
2597
  key.** A bone that carries no attachment is an exact gauge: turn it by ฮด, turn its
2120
2598
  children back by ฮด, and **not one pixel changes**. Anything optimising against pixels
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Rig compiler for Spine โ€” declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {