spine-rigc 0.20.2 → 0.21.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
@@ -508,9 +508,9 @@ work on any frames you have, and `bench` is a repository workflow that needs a c
508
508
  and `bun run fetch-examples`. The reasoning behind all three is in
509
509
  [the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
510
510
 
511
- `build` and `validate` both default to `--profile spine` — the 26 validity rules, which
511
+ `build` and `validate` both default to `--profile spine` — the 27 validity rules, which
512
512
  ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
513
- adds all 41: the other 15 are one renderer's policy and one canvas budget's, and they
513
+ adds all 42: the other 15 are one renderer's policy and one canvas budget's, and they
514
514
  fire on perfectly correct editor-produced Spine data, so reach for that profile when
515
515
  you are shipping into *that* project rather than to be thorough. A report always names
516
516
  the profile it ran and lists what that profile left out.
@@ -576,7 +576,7 @@ letting `A17` blame the editor for the harness's own doing.
576
576
  | 📥 **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
577
577
  | 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
578
578
  | 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
579
- | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 41 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
579
+ | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 42 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
580
580
  | 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
581
581
  | 🗺️ [ROADMAP.md](https://github.com/firejune/rigc/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 has to mean before the number is claimed — conditions rather than a feature list, because direction here comes from what users hit |
582
582
 
@@ -632,7 +632,7 @@ quality."* All six, with their verdicts, are in
632
632
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
633
633
 
634
634
  The whole dossier — the yardstick, `diff` and `check` and what neither of them can
635
- see, every rung, the run viewer, the 41 assertions and the selftest behind them — is
635
+ see, every rung, the run viewer, the 42 assertions and the selftest behind them — is
636
636
  [docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
637
637
  Live rung status is
638
638
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
package/docs/AUTHORING.md CHANGED
@@ -163,7 +163,7 @@ What the flags mean:
163
163
  | `--atlas-in` | `build` only: resolve every part against the **regions of a pre-packed `.atlas`** instead of against loose PNGs. Region geometry is read from the file and sizes are descaled by the page's `scale:`; the atlas is re-emitted into `--out`, re-anchored — **§0.2** |
164
164
  | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** — every `.png` in it is a part, in name order. For `chainfit` it is only where each attachment's image name **resolves**: the candidate decides what the parts are, so extra PNGs are unused and a missing name is refused by name (§12.3) |
165
165
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
166
- | `--profile` | `spine` = the 26 validity rules (**the default**) · `spine-html` = all 41, opt-in |
166
+ | `--profile` | `spine` = the 27 validity rules (**the default**) · `spine-html` = all 42, opt-in |
167
167
  | `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` is the one command that takes it more than once** — repeat it 2–4 times, one per pane, labelled A, B, C, D in the order given; everywhere else a repeat is a typo and is refused |
168
168
  | `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview`, and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote`, so is a name that only *some* candidates have |
169
169
  | `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
@@ -564,6 +564,54 @@ behind it writes literal `x`/`y` instead.
564
564
 
565
565
  **R9 — Nothing is written until every assertion is green.**
566
566
 
567
+ **R10 — The `animations` object is keyed in the editor's order, not in yours, and
568
+ names that have no one order are refused.** Declare animations in whatever order
569
+ reads best; the emit keys them the way the Spine editor does — **natural and
570
+ case-insensitive**. This is the one place rigc
571
+ reorders anything you wrote, and it is not cosmetic: a `slider`'s animation is a
572
+ **name** in JSON and an **ordinal** in the format's binary half, so an editor that
573
+ re-sorts the object repoints every slider whose animation moved index — silently,
574
+ in a file that still parses and still gates green (§3.5.2,
575
+ [#535](https://github.com/firejune/rigc/issues/535)). Nothing else moves: each
576
+ animation's own body is byte-identical either way, and every other collection is
577
+ emitted in the order you gave it.
578
+
579
+ ⚠️ **What is measured about that comparator, and what is not.** The editor sorts
580
+ natural and case-insensitive — measured, two rigs, one axis each:
581
+ `Turn, sweep, wave` came back `sweep, Turn, wave`, and `turn10, turn2, zoom` came
582
+ back `turn2, turn10, zoom` ([#539](https://github.com/firejune/rigc/issues/539)).
583
+ But "natural and case-insensitive" is a **family** of comparators, not one, and
584
+ **four** of its choices have never been measured. rigc emits the order every
585
+ member of that family agrees on, and **refuses the sets where one of the four
586
+ would decide**, naming the pair. So the rule you have to hold is about *names*,
587
+ and it is four things:
588
+
589
+ | Do not let two animation names differ | Because | Instead |
590
+ | --- | --- | --- |
591
+ | by **case alone** (`Turn` against `turn`) | they fold together, so only a tie-break separates them, and nobody has measured which way it breaks | pick one case for all of them, or change a letter |
592
+ | by a **number written two ways** (`turn01` against `turn1`) | `01` and `1` are one number twice; shorter-first, longer-first and lexicographic are all real tie-breaks | write the number one way — with leading zeros or without, but not both |
593
+ | by a **digit run against a word** (`1turn` against `turn`) | comparators differ on whether a number sorts before a word | rename so a run of digits is never compared against a word |
594
+ | by a **separator** — anything that is neither a letter nor a digit (`wave_x` against `wavea`, `wave` against `wave-`) | a collator may treat `-` or a space as ignorable, and `_` sits *between* `Z` and `a`, so folding up and folding down order it oppositely | rename so the first character that differs is a letter or a digit |
595
+
596
+ ⭐ **Capitals and numbered series are not what is refused** — only pairs one of
597
+ those four decides. `Sweep, Turn, Wave, Zoom02, Zoom10` builds, and so does
598
+ `shot1 … shot12`: every member of the family puts each of those sets in one
599
+ order, and that order is what rigc emits. A numbered series that crosses 9 → 10 is
600
+ keyed **1, 2, … 9, 10, 11, 12**, which is what the editor does with it — and is
601
+ not what a codepoint sort does.
602
+
603
+ ✅ **This list had two more rows before
604
+ [#543](https://github.com/firejune/rigc/issues/543), and both were artefacts of
605
+ the emit rather than facts about the editor.** rigc used to key `animations`
606
+ **codepoint-ascending** and refuse every pair codepoint and the editor could order
607
+ differently — which refused a pair that folds the other way (`Turn` against
608
+ `sweep`) and a pair of digit runs of unequal width (`turn10` against `turn2`).
609
+ Those are the only two name sets anybody has ever put through the editor and read
610
+ back, so the tool was refusing precisely the pairs it knew the most about, and its
611
+ only repair was *rename* — the one repair a transcription cannot take. Emitting a
612
+ member of the family instead moves no byte on any set the old rule accepted; it
613
+ just stops refusing the ones it did.
614
+
567
615
  ---
568
616
 
569
617
  ## 3. The rig spec, field by field
@@ -1346,6 +1394,14 @@ carrying here:
1346
1394
  - A physics constraint's five components all default to 0, so one that names none of
1347
1395
  them parses cleanly and does nothing at all. rigc refuses it up front, and `A23`
1348
1396
  catches it from the other side.
1397
+ - An **ik** and a **physics** constraint both carry `ScaleYMode` under the key
1398
+ `scaleY`, spelled `"none"`, `"uniform"` or `"volume"`. It is an enum resolved by
1399
+ `Utils.enumValue`, so only the first letter's case is free and an unrecognised
1400
+ name is assigned as `undefined` with no error; rigc checks it, like the three
1401
+ path modes below. ⚠️ `src/rig.ts` called the physics one **`scaleYMode`** until
1402
+ issue #545 — the runtime's field name rather than the format's key — and nothing
1403
+ read it, so a spec that wrote `scaleYMode` set no mode and said nothing. A rig
1404
+ that still writes it is now refused by name, with `scaleY` beside it.
1349
1405
 
1350
1406
  Every constraint may also carry `skin: true`, which makes it run only under the skin
1351
1407
  that lists it — see §3.4.1, and note that the flag alone does nothing.
@@ -1442,6 +1498,42 @@ parser resolves it in a **second pass** over the constraints array, after the
1442
1498
  animations are read, and a miss throws `Slider animation not found`; rigc refuses it
1443
1499
  where the message can name both files.
1444
1500
 
1501
+ 🔒 **And it is the one field an editor round trip can repoint under you, which is
1502
+ why R10 exists.** In JSON the reference is a name on both sides. In the format's
1503
+ binary half it is an **ordinal** — `constraint.animation = animations[readInt()]`
1504
+ (`SkeletonBinary`) — so an editor holding that ordinal writes back whichever
1505
+ animation now stands at the position. `gallery/look` went into a licensed editor
1506
+ (data version 4.3.26) as `turn, tilt, sweep` with `yaw -> "turn"` and came back
1507
+ `sweep, tilt, turn` with **`yaw -> "sweep"`**: a file that parses, gates green and
1508
+ applies the wrong animation. ⭐ Its second slider is what named the mechanism
1509
+ rather than a second casualty — `tilt` survived because it sat at index 1 in both
1510
+ orderings. rigc now emits animations in the editor's own order so its re-sort
1511
+ moves no index ([#535](https://github.com/firejune/rigc/issues/535)); on the same
1512
+ rig through the same editor that restored `yaw -> "turn"` and took the
1513
+ re-rendered mean absolute error from 10.4655 / 8.4961 / 8.7140 down to
1514
+ 0.3035 / 0.0769 / 0.0588. (`look`'s three names are ones a codepoint sort orders
1515
+ identically, which is what rigc emitted when that trip was measured.)
1516
+
1517
+ ⚠️ The editor's comparator is natural and case-insensitive
1518
+ ([#539](https://github.com/firejune/rigc/issues/539)), and four of its choices are
1519
+ unmeasured — so the emit is that family's order for names none of the four
1520
+ decides, and the rest are a compile error. **R10** has the four shapes to avoid.
1521
+
1522
+ ✅ **What that repair does not reach is a compile error now, not a hazard.** This
1523
+ paragraph used to say that names a codepoint sort and a friendlier one disagree
1524
+ about — `Turn` / `turn`, `turn2` / `turn10` — were where *the hazard returns*, and
1525
+ that nobody had round-tripped such a pair. Somebody has: `Turn, sweep, wave` came
1526
+ back `sweep, Turn, wave` and `turn10, turn2, zoom` came back `turn2, turn10, zoom`
1527
+ ([#539](https://github.com/firejune/rigc/issues/539)). ⇒ rigc no longer leaves
1528
+ that to naming discipline — and since
1529
+ [#543](https://github.com/firejune/rigc/issues/543) it does better than refusing
1530
+ those two, because they are the two sets the editor's answer is **known** for:
1531
+ both are emitted in the order it returned. What is still a compile error is the
1532
+ set whose order turns on one of the four unmeasured choices, printed with both
1533
+ names, which of them decides it, and the rename that settles it. What changed is
1534
+ the price of forgetting: a build that stops, rather than a slider that silently
1535
+ applies the wrong animation.
1536
+
1445
1537
  ⚠️ **The fields of the model you did not choose are refused, not ignored.** The
1446
1538
  parser reads `time` only in the bone-less branch and `property`/`from`/`to`/`scale`/
1447
1539
  `max`/`local` only in the other, so the losing half would be data no runtime ever
@@ -1710,7 +1802,8 @@ is not an array. Every field is optional and each is the payload a firing
1710
1802
 
1711
1803
  Optional with one exception, and only meaningful for rigc's own formations:
1712
1804
  `meshSlots` and `meshTriangles` (the two halves of the mesh budget `A13` measures
1713
- against), `axisBone`, `massBone`, `detached`, `deformMayFold`. Nothing in skeleton
1805
+ against), `axisBone`, `massBone`, `detached`, `deformMayFold`, `editorRoundTrip`.
1806
+ Nothing in skeleton
1714
1807
  JSON records that a
1715
1808
  bone carries a cut's axis or that a parentage is forbidden, so the rig spec says it
1716
1809
  and the validator's archetype assertions read it. **An assertion whose field is
@@ -1748,6 +1841,33 @@ and the entry is gone. ⇒ An exemption whose `why` reads *"known defect, see
1748
1841
  against a fix, not a fix — and the thing that made it repayable was A39
1749
1842
  measuring the ceiling the art could actually take.
1750
1843
 
1844
+ 🎬 **`editorRoundTrip` is the one field here that names a CONSUMER rather than a
1845
+ shape.** Write `"editorRoundTrip": true` when this rig is authored to come back
1846
+ out of the Spine editor — imported, hand-edited, exported — and
1847
+ `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` refuses a physics constraint driving a
1848
+ component that editor cannot hold. `true` is the only accepted value; a `false`
1849
+ would be a key nothing reads ([#545](https://github.com/firejune/rigc/issues/545)).
1850
+
1851
+ ⚠️ **Leaving it out is not a weaker gate, and this is the part worth reading.**
1852
+ rigc's output is not wrong here: a physics constraint driving `rotate` is valid
1853
+ Spine 4.3 that every runtime plays — a cowlick, a tail, an ear — so refusing it by
1854
+ default would be refusing correct data on behalf of a pipeline nobody declared.
1855
+ What a rig that says nothing gets instead is the **SKIP**, and the SKIP names the
1856
+ constraint and the component:
1857
+
1858
+ ```
1859
+ SKIP A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP: the rig "look" does not declare `invariants.editorRoundTrip`, so nothing here is gated against the Spine editor. What is here: physics "whip" drives rotate, and the editor's physics model holds x and y only, so a round trip returns that constraint driving nothing at all (issue #540)
1860
+ ```
1861
+
1862
+ 📏 **Measured, not inferred** ([#540](https://github.com/firejune/rigc/issues/540)):
1863
+ three rigs, twelve constraints, predictions written before the round trip. A lone
1864
+ `y` came back and `x` + `y` together came back — the rule is membership, not arity
1865
+ — while a lone `rotate`, a lone `scaleX` and a lone `shearX` each came back driving
1866
+ **no component at all**, and neither `scaleY` mode rescues `scaleX`. So the fix for
1867
+ a refusal is to drive the constraint in `x`/`y`, or to drop the declaration if this
1868
+ rig never goes near the editor. ⇒ `A23_PHYSICS_CONSTRAINT_EFFECTIVE` is the same
1869
+ loss seen from the far side: it is what fires on the file the editor hands **back**.
1870
+
1751
1871
  🚫 **Do not reach for it to cover a part you have faded out.** A key whose slot
1752
1872
  draws no pixels at that key's own time is already passed over — `A39` measures
1753
1873
  that and says so (§4.11, and the `skipped` line in the `DEFORM` block). Declaring
@@ -3160,6 +3280,38 @@ compiled, and ask only what the one file in front of them can answer. Every
3160
3280
  one per message. (The rig spec's parser predates the convention and its messages
3161
3281
  are prose, so they sit in the second table with everything else.)
3162
3282
 
3283
+ 🚨 **A key neither format has is refused by name, in both files.** Not a row in
3284
+ the table below, because it is not about one field: every object in a rig spec and
3285
+ in a motion spec is checked against the keys its shape actually owns, and a key
3286
+ outside that set stops the build. Issue #545 is why — before it, such a key was
3287
+ never looked at, never mentioned and never emitted, and the build exited 0 with
3288
+ every assertion green. Four keys planted into one physics constraint all vanished,
3289
+ and no line of output named any of them.
3290
+
3291
+ ```
3292
+ rigc compile error: rig.json: constraint "ctl" (physics) has 4 keys this compiler
3293
+ does not read: "scaleYMode" (did you mean "scaleY", "scaleX"?), "scale" (did you
3294
+ mean "scaleX", "scaleY"?), "wobble", "ROTATE" (did you mean "rotate"?). Nothing
3295
+ reads such a key, so it would be dropped from the emitted skeleton in silence —
3296
+ fix the spelling or remove it. Known here: bone, damping, dampingGlobal, fps, …
3297
+ ```
3298
+
3299
+ Read it as a **repair**, not a rule: every stray key on that object is named at
3300
+ once, the closest known spellings come with it (the search is case-insensitive, so
3301
+ a real key in the wrong case leads the list), and the shape's whole key set is
3302
+ printed after. What it will *not* do is guess — a key four edits from anything gets
3303
+ no suggestion, only the set.
3304
+
3305
+ ⚠️ There is **no forward-compatibility escape**, and no `note` field except where
3306
+ one is listed: a rig spec's root, a motion spec's root, an animation, and a
3307
+ `physics` tuning entry. Prose anywhere else has to go in a document, because a key
3308
+ the compiler tolerates is a key it cannot distinguish from one you meant it to
3309
+ read. (The **cut manifest** is deliberately outside this: it is the record of the
3310
+ pipeline that produced the art as much as an input, it carries fields the compiler
3311
+ states outright that it does not read — `roi` — and the fixtures in this repository
3312
+ already give it `note` and `archetype`. It has no shape parse at all, which is the
3313
+ same hole issue #307 closed for the motion spec.)
3314
+
3163
3315
  | Key | Refused when it is not | Why the shape matters |
3164
3316
  | --- | --- | --- |
3165
3317
  | the file itself | a JSON object | the version row below would otherwise report a missing `spec` tag in a file that has no fields at all |
@@ -3273,6 +3425,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3273
3425
  | `skin "S": uses the long form … and also has a key "X"` | §3.4.1 — move the slot inside `attachments` |
3274
3426
  | `animation "A" keys "X" as a path constraint, but the rig declares it as a "slider"` | §4.12 — a timeline group resolves by name AND type; use the field named after the constraint's own type |
3275
3427
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
3428
+ | `N pair(s) of animation names have no one order: … "turn" / "Turn" (case) — they are one name in two cases, and which of them the editor puts first is not measured; rename one of them so they differ by more than letter case` | **R10** — rename until no pair is left. The kind in brackets says which of the editor comparator's four UNMEASURED choices decides the pair: `case` (a pure case tie), `number` (one number written two ways, or a run of digits against a word) or `separator` (make the first character that differs a letter or a digit). rigc keys `animations` in the editor's own comparator — natural and case-insensitive ([#539](https://github.com/firejune/rigc/issues/539), [#543](https://github.com/firejune/rigc/issues/543)) — so a pair that comparator settles is emitted rather than refused, and only the four choices nobody has measured are a compile error; on those, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
3276
3429
 
3277
3430
  ### 5.2 Assertions — the gate
3278
3431
 
@@ -3341,6 +3494,7 @@ The report prints one line per assertion:
3341
3494
  | `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
3342
3495
  | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
3343
3496
  | `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be additive (a slot colour, an attachment swap, a draw order, a sequence), where `"additive": true` is not the fix and one of the two has to go. The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, and which one wins today. Three shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, and two sliders on different properties. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
3497
+ | `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` | both | a physics constraint driving a component the **Spine editor** cannot hold, on a rig that declared `invariants.editorRoundTrip` (§3.7). The editor's physics model holds `x` and `y` only, with no cap on how many at once, so a constraint driving `rotate`, `scaleX` or `shearX` is imported, exported and handed back driving **nothing** — measured over three rigs and twelve constraints with the predictions written first ([#540](https://github.com/firejune/rigc/issues/540)). The detail names the constraint and each component. ⚠️ rigc's own output is correct — every runtime plays a rotation jiggle — so this is opt-in and the default is *not* silence: on a rig that declares nothing it **SKIPs**, and the SKIP names the constraint and the component anyway, so an author learns without having asked. Fix by driving the constraint in `x`/`y`, or by dropping the declaration if the rig never goes near the editor. Disjoint from `A23_PHYSICS_CONSTRAINT_EFFECTIVE` by construction: A23 refuses an **empty** driven set, which is what comes back from the editor, and this refuses a non-empty one that will not survive going in. **SKIP** also when the rig declares the editor and carries no physics constraint at all |
3344
3498
 
3345
3499
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
3346
3500
  clauses are gated by profile.
@@ -3365,6 +3519,7 @@ says so, because a deferral without its reason is a wall rather than a work item
3365
3519
  | constraint `type` of anything else | `constraint type "X" is not one Spine 4.3 knows. The five are: ik, transform, path, physics, slider.` — all five are emitted, so this is a typo, and a typo is what the parser drops in silence |
3366
3520
  | a path attachment's `lengths` | `"lengths" is not authored — rigc measures the setup arc length of each curve off the geometry` (§3.4). Not a deferral: a second copy of a number the vertices already fix |
3367
3521
  | a `deform` timeline on a path attachment | `a path attachment does have a vertex array, and rigc does not key it yet` — the format allows it and an animated track is a real idiom, but a deformed path invalidates the `lengths` a `constantSpeed: false` traversal reads. Move the curve by posing the bones its vertices are bound to |
3522
+ | any key neither format has, anywhere in either file | `<object> has a key this compiler does not read: "x" (did you mean "y"?) … Known here: …` (§5.1). Not a deferral either: a key nothing reads is a value you wrote and the emitted skeleton does not contain |
3368
3523
 
3369
3524
  Two more limits that are not errors but will shape what you can attempt:
3370
3525
 
@@ -4817,6 +4972,57 @@ low figure as a miss — say in the log that the art did not carry them.
4817
4972
  `default`"* and *"bones are ordered so that the parent always comes before a child
4818
4973
  bone"* — [JSON format](http://esotericsoftware.com/spine-json-format). §3.4.
4819
4974
 
4975
+ 🔬 **The editor re-keys every name-keyed OBJECT and leaves every ARRAY alone.**
4976
+ Read off its export of a rigc build (Spine 4.3.26, `gallery/look`): the
4977
+ `animations` object, a skin's 24 `attachments` slot keys and two animations' 16
4978
+ and 2 bone-timeline keys all came back sorted, while the 30 `bones`, 24 `slots`
4979
+ and 3 `constraints` — arrays — came back in the build's own order, element for
4980
+ element, and each slider kept its place among them.
4981
+
4982
+ ⚠️ **The order it sorts them into is natural and case-insensitive, not
4983
+ codepoint.** This paragraph said codepoint until
4984
+ [#539](https://github.com/firejune/rigc/issues/539) measured it: `Turn, sweep,
4985
+ wave` came back `sweep, Turn, wave` and `turn10, turn2, zoom` came back
4986
+ `turn2, turn10, zoom`. The corpus says the same thing and always did — of its 105
4987
+ name-keyed collections, 102 are consistent with a codepoint sort and **3 are
4988
+ not**: `spineboy-pro.json` keys `portal-flare9` *before* `portal-flare10`, which
4989
+ no codepoint sort produces. Every natural comparator reproduces all 105. (The
4990
+ population is every object the format keys by a name and that carries more than
4991
+ one key, deform blocks counted at each of their three levels;
4992
+ [`src/compile.ts`](../src/compile.ts) states it in full beside
4993
+ `editorAnimationOrder`, so the count can be re-taken rather than trusted.)
4994
+ ⇒ in rigc: only `animations` is emitted sorted (R10), because it is the one
4995
+ object measured here whose ORDER is also an index space — every reference into
4996
+ the re-sorted *other* objects is by name on both sides, so nothing moves when
4997
+ they are re-keyed. rigc emits **that comparator's own order** and refuses the name
4998
+ sets on which its leading-zero, case-tie, digit-against-word or separator
4999
+ behaviour — the four choices still unmeasured — would decide a pair. Sorting the
5000
+ 105 collections that way reproduces **105 of 105**, the three codepoint cannot
5001
+ included, and refuses none of them; the codepoint rule that stood until
5002
+ [#543](https://github.com/firejune/rigc/issues/543) reproduced 102 and refused
5003
+ those same 3.
5004
+
5005
+ ✅ **`events` is re-keyed too, and the references into it survive it.** The same
5006
+ session measured it: `zebra, mike, alpha` came back `alpha, mike, zebra`, and the
5007
+ firings still resolved **by name** — `0.3 -> mike`, `0.6 -> alpha`, payloads
5008
+ intact (#539). So the editor treats `events` and `animations` differently, and
5009
+ rigc emits events in the order you declare them.
5010
+
5011
+ ⚠️ **Read *"leaves every ARRAY alone"* above as bones, slots and constraints —
5012
+ `skins` is the array that round trip was not taken over.** `gallery/look` declares
5013
+ one skin, as does every other rig in this repository and all twelve editor exports
5014
+ in `examples/`, and a one-element array comes back in order whatever the editor
5015
+ does to it — so nothing here is
5016
+ evidence about `skins`, and a pull request that once called them *measured
5017
+ preserved* was reading a vacuous result ([#544](https://github.com/firejune/rigc/issues/544)).
5018
+ It matters because `skins` carries ordinals in the binary half too —
5019
+ `skins[readInt()]` for an attachment timeline and a linked mesh's skin index — so
5020
+ a re-order there would repoint them the way the `animations` re-key repoints a
5021
+ slider. ⛔ And it cannot be measured today: the editor refuses a four-skin rig on
5022
+ import without writing a project file or printing a word
5023
+ ([#541](https://github.com/firejune/rigc/issues/541)). ⇒ **If you author more than
5024
+ one skin, nothing on this page says the editor survives it.**
5025
+
4820
5026
  ### 10.2 Draw order
4821
5027
 
4822
5028
  📗 **An overlap change is a draw-order key.** The draw order *"can be keyed"*, and
package/docs/FACE.md CHANGED
@@ -1180,12 +1180,83 @@ breaks the moment the two share a target — in the worked example both `turn` a
1180
1180
  `tilt` key `headroll`. §7's paragraph on sliders is the mechanism and
1181
1181
  `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` is the refusal.
1182
1182
 
1183
- ⚠️ **What none of this measures: the Spine editor.** No editor export in this
1184
- repository carries a slider, so whether the editor preserves two of them, their
1185
- `additive` and `local` flags, and their order in the constraints array is
1186
- **unknown**. `tools/editor_roundtrip.ts` on a machine with a licensed editor is
1187
- what would answer it, and until somebody runs it the editor half of a parameter
1188
- axis is untested. The runtime half is not: every figure above came back through
1183
+ ✅ **The editor half, measured.** This paragraph said *unknown* until the round
1184
+ trip was taken with `tools/editor_roundtrip.ts` on a licensed editor (data
1185
+ version 4.3.26) against a 4.3.13 build of
1186
+ [`gallery/look`](https://github.com/firejune/rigc/tree/main/gallery/look) — this
1187
+ subsection's worked case, not the `gallery/portrait` this page names at the top,
1188
+ which declares no constraints at all and so can carry no slider. What it found:
1189
+
1190
+ - **Both sliders come back, and the parameter axis survives.** `additive`,
1191
+ `local`, `bone`, `property`, `from`, `max` and `scale` are identical field for
1192
+ field, and the two keep their places in the `constraints` array. `mix: 1` and
1193
+ `to: 0` are dropped, and those are the format's own defaults (`SkeletonJson`
1194
+ reads `mix` as 1 and `to` as 0 when absent) — an elision, not a loss.
1195
+ - ⚠️ **The animation each slider *names* did not come back, until rigc changed
1196
+ what it emits.** The editor re-sorts the `animations` object and a slider's
1197
+ animation is an ordinal in the format, so `yaw -> "turn"` returned as
1198
+ `yaw -> "sweep"` — the first animation of the sorted list
1199
+ ([#535](https://github.com/firejune/rigc/issues/535)). rigc now emits
1200
+ animations in the editor's own order — natural and case-insensitive
1201
+ ([#539](https://github.com/firejune/rigc/issues/539),
1202
+ [#543](https://github.com/firejune/rigc/issues/543)); this rig's names are ones
1203
+ a codepoint sort orders identically, which is what it emitted at the time of
1204
+ the round trip below. On the same rig through the same editor that
1205
+ restored `yaw -> "turn"` and took the re-rendered mean absolute error from
1206
+ 10.4655 / 8.4961 / 8.7140 (`sweep` / `tilt` / `turn`) to
1207
+ 0.3035 / 0.0769 / 0.0588, worst drift 16.535 px to 3.947 px.
1208
+ - 🚨 **The physics constraint on the cowlick comes back driving nothing.**
1209
+ `rotate: 1` is absent from the export, and an absent `rotate` parses as **0**
1210
+ (`SkeletonJson`), so the returned file states *drives nothing* rather than
1211
+ omitting a default — which is why `A23_PHYSICS_CONSTRAINT_EFFECTIVE` refuses it
1212
+ by name. Independent of the ordering defect, and filed as
1213
+ [#536](https://github.com/firejune/rigc/issues/536).
1214
+
1215
+ ✅ **Why, measured since.** It is not elision and not a defect in one field:
1216
+ the editor's physics model holds `x` and `y` and nothing else, with no limit on
1217
+ how many at once. Three rigs, twelve constraints, predictions written before the
1218
+ round trip — a lone `y` came back, `x` and `y` together came back, and a lone
1219
+ `rotate`, a lone `scaleX` and a lone `shearX` each came back as **no components
1220
+ at all**, with every constraint's fixed-point `strength` returning exactly so a
1221
+ silent harness failure could not read as a finding
1222
+ ([#540](https://github.com/firejune/rigc/issues/540)). ⇒ **A rotation-driven
1223
+ jiggle does not survive the editor, and no `scaleY` mode substitutes for it.**
1224
+ ⚠️ Still open on #536: whether the loss happens at import or at export. The
1225
+ project file's bytes cannot settle it — it carries derived float32s that no
1226
+ input declares — and the answer is invisible to an author either way.
1227
+
1228
+ ✅ **And the gate now says so before the trip, not after.** A face rig that is
1229
+ authored to come back out of the editor declares
1230
+ `"invariants": { "editorRoundTrip": true }` (AUTHORING §3.7) and
1231
+ `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` refuses the constraint by name at build
1232
+ time, with the fix in the message: drive it in `x`/`y`, or drop the declaration.
1233
+ ⚠️ A rig that declares nothing is **not** silent either — A41 SKIPs and the skip
1234
+ names the constraint and the component, which is the whole reason the rule is
1235
+ opt-in rather than default-off. rigc's own output was never wrong here: a
1236
+ rotation jiggle is valid Spine 4.3 that every runtime plays, and refusing it for
1237
+ everybody would be refusing correct data on behalf of one consumer. ⇒ A23 and
1238
+ A41 are the same loss from opposite sides of the trip: A41 fires on what goes
1239
+ in, A23 on what comes back.
1240
+ - 🔸 Unexplained: `diff` reports `animations.curve_kinds` moved on **196 of 200**
1241
+ keys in every round trip taken, the clean one included. Visually small once the
1242
+ ordering is fixed — but it is 98% of the keys, and *small* is not *explained*.
1243
+
1244
+ ✅ **The two things this paragraph said it still did not measure have since been
1245
+ measured, and they came out opposite ways** — [#544](https://github.com/firejune/rigc/issues/544)
1246
+ is the card for having left the sentence standing:
1247
+
1248
+ - **More than one event is safe.** The editor re-keys `events` the way it re-keys
1249
+ `animations` — `zebra, mike, alpha` came back `alpha, mike, zebra` — but every
1250
+ firing resolved **by name**, `0.3 -> mike` and `0.6 -> alpha`, payloads intact
1251
+ ([#539](https://github.com/firejune/rigc/issues/539)). The ordinal shape does
1252
+ *not* bite here, and rigc emits events in the order you declare them.
1253
+ - **More than one skin is worse than unmeasured.** A four-skin rig builds green,
1254
+ parses in `spine-core`, and the editor **refuses to import it** — no project
1255
+ file, no message ([#541](https://github.com/firejune/rigc/issues/541)). So there
1256
+ is no export to read, and every figure on this page was taken on a rig carrying
1257
+ exactly one skin (AUTHORING §10.1).
1258
+
1259
+ The runtime half was never in doubt: every figure above came back through
1189
1260
  `spine-core`.
1190
1261
 
1191
1262
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.20.2",
3
+ "version": "0.21.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": {