spine-rigc 0.25.2 → 0.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -519,9 +519,9 @@ work on any frames you have, and `bench` is a repository workflow that needs a c
519
519
  and `bun run fetch-examples`. The reasoning behind all three is in
520
520
  [the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
521
521
 
522
- `build` and `validate` both default to `--profile spine` — the 27 validity rules, which
522
+ `build` and `validate` both default to `--profile spine` — the 28 validity rules, which
523
523
  ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
524
- adds all 42: the other 15 are one renderer's policy and one canvas budget's, and they
524
+ adds all 43: the other 15 are one renderer's policy and one canvas budget's, and they
525
525
  fire on perfectly correct editor-produced Spine data, so reach for that profile when
526
526
  you are shipping into *that* project rather than to be thorough. A report always names
527
527
  the profile it ran and lists what that profile left out.
@@ -663,7 +663,7 @@ letting `A17` blame the editor for the harness's own doing.
663
663
  | 📥 **[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 |
664
664
  | 🤖 **[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 |
665
665
  | 🔬 **[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 |
666
- | 🎓 **[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 |
666
+ | 🎓 **[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 43 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 |
667
667
  | 📋 [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 |
668
668
  | 🗺️ [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 |
669
669
 
@@ -719,7 +719,7 @@ quality."* All six, with their verdicts, are in
719
719
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
720
720
 
721
721
  The whole dossier — the yardstick, `diff` and `check` and what neither of them can
722
- see, every rung, the run viewer, the 42 assertions and the selftest behind them — is
722
+ see, every rung, the run viewer, the 43 assertions and the selftest behind them — is
723
723
  [docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
724
724
  Live rung status is
725
725
  [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 27 validity rules (**the default**) · `spine-html` = all 42, opt-in |
166
+ | `--profile` | `spine` = the 28 validity rules (**the default**) · `spine-html` = all 43, 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` |
@@ -1900,6 +1900,14 @@ dial, the gate swings.
1900
1900
  { "name": "reveal", "type": "slider", "animation": "curtain", "time": 0 }
1901
1901
  ```
1902
1902
 
1903
+ ⚠️ **This branch takes neither of the repairs the bone branch takes.**
1904
+ `Slider.update` applies `Math.max(0, time)` and the `loop` wrap *inside* its
1905
+ `if (bone !== null)`, so a bone-less slider's time is used exactly as the
1906
+ timeline left it: a negative time is not clamped to the first frame, it is a time
1907
+ before the animation starts, where `Animation.apply` leaves the pose it found
1908
+ untouched. That is the same picture as the first frame only when the first frame
1909
+ *is* the rest pose. [measured] `PS140` in `selftest.ts`.
1910
+
1903
1911
  | Field | Meaning |
1904
1912
  | --- | --- |
1905
1913
  | `animation` | **required.** An animation the **motion spec** declares |
@@ -2006,13 +2014,95 @@ it, on every frame, with the gate green — the second reason to write
2006
2014
  later one. `PS130` in `selftest.ts` poses both models rather than quoting the
2007
2015
  runtime, and [`docs/FACE.md`](FACE.md) §8 is the same rule on a face's two axes.
2008
2016
 
2009
- ⚠️ **And `"additive": true` is not always available.** Only some timelines support
2010
- additive application at all: bone, deform, transform-constraint, path `position`,
2011
- physics `wind`/`gravity`, and a slider's own `mix`. A **slot colour, an attachment
2012
- swap, a draw order or a sequence ignores the flag entirely**, so two sliders
2013
- sharing one of those overwrite each other whatever you write. A40 names that case
2014
- separately, because the fix is different: key such a property from one slider
2015
- only, or move both edits into the single animation one slider applies.
2017
+ ⚠️ **And `"additive": true` is not always available.** What composes is bone,
2018
+ deform, transform-constraint, path `position` and path `mix`, physics
2019
+ `wind`/`gravity`, and a slider's own `mix` and `time`. The rest ignore the flag —
2020
+ a slot colour, an attachment swap, a draw order and a sequence, and also an **ik
2021
+ constraint's mix**, a path's `spacing`, and every physics timeline except those
2022
+ two — so two sliders sharing one of those overwrite each other whatever you
2023
+ write. ⚠️ The four spelled out here used to read as the whole of the complement
2024
+ and they are examples of it; `A40` was never reading a list, which is why it
2025
+ refuses the ik case this sentence did not name.
2026
+
2027
+ 🚨 **That list is not `Timeline.additive`, and this is what it cost to learn.**
2028
+ The runtime's own flag says a class "supports being applied additively", and on
2029
+ two classes it is not what the class does: `PathConstraintMixTimeline` and
2030
+ `SliderTimeline` declare themselves non-additive and their `apply` passes the
2031
+ `add` argument straight through anyway — every other non-additive timeline either
2032
+ hardcodes `false` in the call, zeroes `add` first, or never reads it. So two
2033
+ additive sliders keying one path constraint's `mix`, or one slider's `time`,
2034
+ **do** compose, as the same sum as everything else above, and `A40` refused both
2035
+ by name with a message saying `"additive": true` would not compose them. ✅ **It
2036
+ no longer reads the flag: it poses each shared timeline twice with `add` set and
2037
+ reads whether the second application accumulated**
2038
+ ([#655](https://github.com/firejune/rigc/issues/655)), so the two rigs above pass
2039
+ and the message names what the class was measured to do. [measured] `PS143` in
2040
+ `selftest.ts` poses all thirty spellings of the motion vocabulary under two
2041
+ additive sliders and prints the flag beside the behaviour; `PS145` holds the
2042
+ probe's verdict to that same pose on every one of them; `PS140` holds the `time`
2043
+ case to a grid.
2044
+
2045
+ ⭐ **The same measurement retired a refusal nothing could have distinguished.**
2046
+ Two sliders whose animations both fire **events** were refused as sharing a
2047
+ property — and a slider fires no event at all: it applies its animation with
2048
+ `firedEvents` null, and `EventTimeline.apply` returns on that. The probe's third
2049
+ answer is *this timeline moved no pose at all*, so an events pair, and a physics
2050
+ `reset` pair with it, are simply not findings. Nothing else changed: a shared
2051
+ slot colour, attachment, draw order, ik mix, path `spacing` or physics property
2052
+ is refused exactly as before.
2053
+
2054
+ ⇒ **And the `constraints` array decides twice, for two different reasons.**
2055
+ *Overwriting* has a direction: the slider **later in the array** puts its own
2056
+ animation's value there and the earlier one contributes nothing at all, at every
2057
+ reading of either dial, with both flags set to `true`. And the array is also the
2058
+ **update order**, for **every kind of constraint** — `Skeleton.updateCache` walks
2059
+ it and each constraint's `sort` pushes itself as it is reached — so a property a
2060
+ slider keys is read by the constraint that owns it only if the slider comes
2061
+ **first**. A dial that drives the authority of an earlier slider, the mix of an
2062
+ earlier ik constraint, the `position` of an earlier path constraint or the `wind`
2063
+ of an earlier physics constraint writes a value nothing reads again, and what
2064
+ that constraint drives is dead at every position of the dial.
2065
+
2066
+ ✅ **That second one is a refusal by name since
2067
+ [#658](https://github.com/firejune/rigc/issues/658), and it covers every
2068
+ constraint kind since [#665](https://github.com/firejune/rigc/issues/665).**
2069
+ `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` names the driving slider, the
2070
+ driven constraint and its kind, the property, both `constraints` indices, the
2071
+ runtime class whose `update` does the reading and the animation the key sits in,
2072
+ and its repair is the reorder. It is a rule of its own rather than a clause of
2073
+ `A40` because `A40`'s population is the sliders whose `mix` nothing keys — the
2074
+ exclusion *is* the shape of the hole — and not a clause of `A37`, whose `keyedBy`
2075
+ asks whether some animation keys the `mix` and never which one.
2076
+ ⭐ **The runtime repairs this for bones and not for constraints**, which is why an
2077
+ author cannot reason it out from the bone case: `Slider.sort` clears `sorted` on
2078
+ every bone its animation keys so those bones re-sort *after* the slider, while
2079
+ for a constraint it calls `skeleton.constrained(...)`, which swaps the pose the
2080
+ constraint is read through and moves nothing in the update cache. 🔒 The same
2081
+ refusal covers a slider keying its **own** `mix` or `time` — the two indices
2082
+ equal. `Slider.update` reads both off the applied pose before it applies the
2083
+ animation, so a slider muted at setup whose own animation keys its `mix` up never
2084
+ runs at all and `A37` reports green over it.
2085
+
2086
+ ⚠️ **The constraint's own pose holds the number either way, which is why nothing
2087
+ found this by reading a value back.** In both array orders the keyed property
2088
+ reads back exactly as written — [measured] an ik mix `0.000000..1.000000`, a path
2089
+ `position` `0.000000..0.900000`, a physics `wind` `0.000000..400.000000`, each
2090
+ identical in the two orders — and what changes is what the constraint *drives*:
2091
+ `0.000e+0` of travel against `1.662e+1`, `1.620e+2` and `4.256e+2` the other way
2092
+ round. 🔸 **One spelling is outside the rule and a reorder does not repair it**:
2093
+ a `physics` `reset` key. It writes no pose and fires only when a frame time is
2094
+ crossed, and a slider applies its animation at a single instant — so the key
2095
+ never fires in either order, and the SKIP says so rather than advising a move
2096
+ that would change nothing. [measured] `PS139`, `PS140`, `PS156`, `PS157`,
2097
+ `PS158`, `PS159` and `PS160` in `selftest.ts`.
2098
+
2099
+ Swap the two array entries and the answer swaps with them — it is the
2100
+ array that decides, not the flags and not which animation the file names first
2101
+ (`PS135` in `selftest.ts` poses four such targets both ways; `PS136` poses the
2102
+ three that do compose, and they are the same sum §3.5.2 states, over each target's
2103
+ own setup value). A40 names this case separately, because the fix is different:
2104
+ key such a property from one slider only, or move both edits into the single
2105
+ animation one slider applies.
2016
2106
 
2017
2107
  #### 3.5.2.1 What each `property` can actually be read AS
2018
2108
 
@@ -2047,14 +2137,28 @@ runtime says so. [measured] against `spine-core` 4.3.13, one reader at a time
2047
2137
  `Math.sqrt(a² + c²)`, so a bone at `scaleX: −1` reads **`+1`**, not `−1`: a
2048
2138
  squash axis driven through negative scale gets the mirror of the dial you wrote.
2049
2139
  The floor `0` is *reached*, not approached — a bone whose own scale or whose
2050
- parent's is 0 reads exactly 0 — so a range whose bottom is exactly 0 is fine and
2051
- one that dips below it is dead.
2140
+ parent's is 0 reads exactly 0 — so a range whose bottom is exactly 0 is fine.
2141
+ 🚨 One that dips below it is not *dead*, it **folds**: −2 and +2 read the same
2142
+ number, select the same frame and pose the same face, so the axis doubles back
2143
+ on itself about the point it should have passed through. Such a range is
2144
+ **refused at compile** (§3.5.2.2, beside the `rotate` circle). [measured]
2145
+ `PS138` sweeps both halves and poses them; `PS151` poses the fold on a dial the
2146
+ refusal leaves standing — a legal `0`..`4` window turned below 0 by a consumer
2147
+ applies the same time at −2 and +2 to the bit, while the same mapping read
2148
+ `local: true` is on its own signed mapping at every cell.
2052
2149
  - **`shearY` under `local: false` wraps like `rotate` does, and worse.** It is a
2053
2150
  difference of two `atan2` calls, so at any one bone orientation the readable
2054
2151
  window is 360° wide — `(−270 − θx, 90 − θx]`, where `θx` is the bone's world
2055
2152
  x-axis angle. The bound in the table is the union over every orientation. ⇒ the
2056
2153
  seam is **not at a fixed value of the driven field**; it is wherever the bone is
2057
2154
  pointing. Prefer `local: true` for a shear axis.
2155
+ ⚠️ **Nothing refuses a `shearY` range, and that is deliberate.** The reader
2156
+ keeps its sign — it does not fold the way the two `scale` readers do — and what
2157
+ it does instead is not a fact a rig spec holds: [measured] `PS155` sweeps three
2158
+ orientations across their own seams and each wraps by exactly one turn at
2159
+ `90° − θx` (90°, 45° and 150° for a dial bone at 0°, 45° and −60°), which any
2160
+ animation a consumer writes can move. A range rule here would have to name a
2161
+ seam the compiler cannot know, and would pass the case that actually breaks.
2058
2162
  - **The bounds are not round numbers because `MathUtils.PI` is `3.1415927`** — the
2059
2163
  float32 π of the reference runtime. Every degree in spine-core passes through
2060
2164
  `180 / 3.1415927`, so a full turn converts as `359.99999468178214` and a bone at
@@ -2176,6 +2280,37 @@ not on that path at all. The alternative each refusal leaves open is to move the
2176
2280
  range inside the circle (a neutral at 180°, say), which is the only form
2177
2281
  `local: false` can express.
2178
2282
 
2283
+ 🚨 **The two world `scale` dials have a floor at 0, and a range dipping below it
2284
+ is refused too — for a different reason than the circle's.** `FromScaleX.value`
2285
+ is `Math.sqrt(a² + c²)` and `FromScaleY.value` is `Math.sqrt(b² + d²)`, a
2286
+ **magnitude** with the driven field in both terms, so the positions below 0 are
2287
+ not unreachable, they are *already taken*: [measured] with `from: 0, to: 0.5,
2288
+ scale: 0.25` over a 1 s animation the window is −2..+2, and the dial at −2.000,
2289
+ −1.500, −1.000 and −0.500 applies **1.000000 s, 0.875000 s, 0.750000 s and
2290
+ 0.625000 s** — the same six decimals +2.000, +1.500, +1.000 and +0.500 apply, and
2291
+ the same posed face. The refusal names the arc that is written twice:
2292
+
2293
+ > Positions below 0 read as the mirror of positions above it: 2.000 of the range below 0 repeats 0.000..2.000, which is 0.500s..1.000s of the animation, in reverse.
2294
+
2295
+ - **A bottom of exactly 0 is legal**, because that floor is *reached*: a 0..4
2296
+ window builds, gates green and sweeps the whole animation. A window a
2297
+ thousandth of a unit below it is refused.
2298
+ - **A range lying wholly below 0 gets its own sentence** — *"the whole 4.000 of
2299
+ this range is below 0, so it reaches none of the animation's 1s"* — and that
2300
+ figure is the width **under** the floor, not the reach to the far end, the same
2301
+ distinction [#434](https://github.com/firejune/rigc/issues/434) drew for the
2302
+ circle.
2303
+ - **`loop: true` changes nothing here**, unlike the circle: a fold is not a
2304
+ clamp, so there is no loop branch in the message. [measured] the same ±1.500
2305
+ pair applies 1.875000 s either way.
2306
+ - **The repairs** are `"local": true`, which reads `source.scaleX` signed so the
2307
+ negative half drives the *first* half of the animation ([measured] −2 →
2308
+ 0.000 s, +2 → 1.000 s), or moving the range so it does not dip below 0.
2309
+
2310
+ [measured] `PS151`–`PS154` in `selftest.ts`. `shearY` has no such rule and
2311
+ §3.5.2.1 says why: that reader keeps its sign and wraps at a seam the bone's own
2312
+ orientation places, which a rig spec does not hold.
2313
+
2179
2314
  ⚠️ **An artifact can still carry a dead range** — one exported from the editor,
2180
2315
  hand-edited, or built by an older rigc. `A39` reports that from the artifact side
2181
2316
  as a key at a time no dial selects (§4.11.4); the compile refusal above is what
@@ -2379,11 +2514,24 @@ time puts it here.
2379
2514
  | `transform` | transform constraint timelines — §4.10. Same reason |
2380
2515
  | `deform` | deform timelines — §4.11. Same reason |
2381
2516
 
2382
- `groups` (`name → [member, …]`) lets one track target several bones or slots at
2383
- once; `lag` shifts every key of a track, and `stagger` adds a per-member delay in
2384
- member order. **Member order is load-bearing** — it is what `stagger` counts and
2385
- what a per-member value map is read against — so a group that names a member
2386
- twice is a compile error, and so is one that names none.
2517
+ `groups` (`name → [member, …]`) lets one track target several bones, slots or
2518
+ physics constraints at once; `lag` shifts every key of a track, and `stagger`
2519
+ adds a per-member delay in member order. **Member order is load-bearing** — it is
2520
+ what `stagger` counts and what a per-member value map is read against — so a
2521
+ group that names a member twice is a compile error, and so is one that names none.
2522
+
2523
+ ⭐ **Which of the three a group's members are is decided by the `property`, not
2524
+ by the group.** A group declares names and nothing else; the compiler reads the
2525
+ property first — a physics timeline makes the members physics constraints, one of
2526
+ a bone's ten makes them bones, and `attachment`/`rgba` makes them slots — and then
2527
+ resolves every member against the rig as that. So a `group` is the one target
2528
+ where the property picks the family rather than the other way round (§4.4's ⭐ is
2529
+ about the three **constraint** families, which are picked by the field), and a
2530
+ property that is in none of the three tables is refused naming the group and all
2531
+ three vocabularies rather than being read as any of them
2532
+ ([#661](https://github.com/firejune/rigc/issues/661); §4.4). A group whose members
2533
+ are not all of one family is not refused as such: the first member that is not
2534
+ what the property made it is the one named.
2387
2535
 
2388
2536
  A group track's keys need not give every member the same value: `v` may be a map
2389
2537
  keyed by member name, or a `derive` model the compiler evaluates per member.
@@ -2400,6 +2548,12 @@ and `{ "path": "ride", "property": "mix" }` are different timelines with the sam
2400
2548
  property name — which is why the constraint's name goes in a field named after its
2401
2549
  type rather than in a shared `constraint` key.
2402
2550
 
2551
+ ⚠️ **`group` is the one exception, and the reason it is one is that it names no
2552
+ family:** a group is a list of member names, so the property is all there is to
2553
+ read — it decides whether those members are bones, slots or physics constraints
2554
+ (§4.3), and a property in none of those three tables is refused with all three
2555
+ lists rather than resolved as any of them.
2556
+
2403
2557
  Three families are **not** tracks and sit beside `tracks` instead — `ik` (§4.9),
2404
2558
  `transform` (§4.10) and `deform` (§4.11). The reason is the key rather than the
2405
2559
  target: a track's key carries one `v`, and those three carry named fields each
@@ -2424,6 +2578,91 @@ a deform). Folding them in would make `v` mean four different things depending o
2424
2578
  Translate values are **relative to the bone's setup position**; scale values are
2425
2579
  multipliers where `1` is setup; rotation is in degrees.
2426
2580
 
2581
+ ⚠️ **A `bone` track's `property` is one of the ten above, and anything else is a
2582
+ compile error** — `animation "A" bone "B" has no timeline "P" (it has: translate,
2583
+ translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate)`,
2584
+ §5.1's row. The ten are the two rows above read as one list, in the order the
2585
+ message prints them, and they are the emitter's own dispatch table (`BONE_TRACKS`
2586
+ in `src/compile.ts`): `resolveTargets` asks that table which family a track
2587
+ belongs to, `compileValueTrack` writes a key out of the shape it finds there, and
2588
+ the refusal prints `Object.keys` of the same object — so what you are told a bone
2589
+ accepts is what it accepts.
2590
+
2591
+ - Until [#656](https://github.com/firejune/rigc/issues/656) it printed no list at
2592
+ all: the refusal read `bone "B" cannot take slot property "P"`, which named the
2593
+ **slot** family for whatever you had written — `wobble`, `translateX`, `rgb` —
2594
+ and told you nothing about what a bone does take. Nothing wrong reached disk
2595
+ then either, because the dispatch was already this table; what was missing was
2596
+ the way forward.
2597
+ - `rgba` and `attachment` are the only two names that sentence was ever right
2598
+ about, and for those the redirect survives as a clause beside the list:
2599
+ `. "rgba" is a slot timeline — put the name in "slot"`. It is read off
2600
+ `SLOT_TRACKS`, so the two halves of the message cannot drift apart. A
2601
+ **constraint** property written on a bone track (`mix`, `inertia`, `position`,
2602
+ …) never reaches this refusal at all — it is refused first, by the row that
2603
+ names the field its constraint's name belongs in (§4.12).
2604
+ - **Nothing derives this page's copy of the ten from the table.** What keeps the
2605
+ two in step is the control that quotes the message — `RF26` in `selftest.ts` —
2606
+ which goes red if the list ever widens without this page moving with it, and
2607
+ `RF27` holds the slot clause the same way. The selftest's spelling census
2608
+ (`PS144`) reads the ten off the same message and compares them against the ten
2609
+ it actually poses, both ways, which is what makes the list checkable at all: it
2610
+ was stated there too until the refusal had something to state.
2611
+
2612
+ ⚠️ **A `slot` track's `property` is one of the two above, and anything else is a
2613
+ compile error** — `animation "A" slot "X" has no timeline "P" (it has:
2614
+ attachment, rgba)`, §5.1's row. Until
2615
+ [#650](https://github.com/firejune/rigc/issues/650) it was not: the emitter had a
2616
+ branch for `attachment` and wrote **everything else** as an rgba timeline under
2617
+ the name you gave it, so a track spelled `sequence` compiled, emitted
2618
+ `slots.X.sequence` with rgba keys, and was refused one stage later by the gate
2619
+ (`A00_ROUNDTRIP_PARSE: threw: Invalid timeline type for a slot`) — while the
2620
+ one-channel spelling of the same mistake was refused at compile as `rgba value
2621
+ needs 4 channels, got 1`, a message about a key you had not written.
2622
+
2623
+ - The pair is the emitter's own dispatch table (`SLOT_TRACKS` in
2624
+ `src/compile.ts`): `compileTrack` reads it to pick its branch, and the refusal
2625
+ prints `Object.keys` of the same object, so what you are told a slot accepts
2626
+ is what it accepts.
2627
+ - **Nothing derives this page's copy of that pair from the table**, and the list
2628
+ is two names long: no `DQ*`/`RD*`/`CUR*` control reads §4.4 (the only gated
2629
+ table on this page is §3.5.2.1's, held by `RD01`–`RD06`). What keeps the two
2630
+ in step is the control that quotes the message — `RF23` in `selftest.ts` —
2631
+ which goes red if the accepted pair ever widens without this page moving with
2632
+ it.
2633
+ - The format has four more slot timelines (`rgb`, `alpha`, `rgba2`, `rgb2`) and
2634
+ rigc emits none of them, so their names are refused here too; `A12_NO_DARK_COLOR`
2635
+ refuses the last two in a file rigc did not write (SPEC_COVERAGE §2.1).
2636
+ `sequence` is a timeline on an **attachment**, not on a slot, and rigc does
2637
+ not emit that either.
2638
+
2639
+ ⚠️ **A `group` track's `property` is one of those two lists or the physics one,
2640
+ and anything else is a compile error** — `animation "A" group "G" has no timeline
2641
+ "P" (a bone group has: …; a slot group has: …; a physics constraint group has:
2642
+ …)`, §5.1's row. It is the only refusal on this page that prints **three** lists,
2643
+ and the reason is §4.3's: a group's family is decided by the property, so a
2644
+ property no table claims leaves the compiler with no family to answer for. All
2645
+ three come from the objects the dispatch reads — `BONE_TRACKS`, `SLOT_TRACKS` and
2646
+ `PHYSICS_TRACKS` in `src/compile.ts` — and the refusal is raised in
2647
+ `resolveTargets`, after the group's own existence check and before any member is
2648
+ resolved against the rig.
2649
+
2650
+ - Until [#661](https://github.com/firejune/rigc/issues/661) a group of **bones**
2651
+ with a misspelled bone property read `animation "A" targets unknown slot "M"`:
2652
+ with no table claiming the property the track fell through to the slot branch,
2653
+ and what you were told was that the first member is not a slot — on a file that
2654
+ named neither a slot nor that member. A group of **slots** got §4.4's slot row
2655
+ instead (`slot "M" has no timeline "P" (it has: attachment, rgba)`), which is
2656
+ true of the member and names one family out of three on a track whose family
2657
+ nothing had determined.
2658
+ - **A constraint property never reaches it.** `position`, `spacing` and `time` are
2659
+ refused first with the field their constraint's name goes in (§4.12), and a
2660
+ physics timeline spelled correctly is not an error at all — a group of physics
2661
+ constraints is how several are tuned in one track.
2662
+ - `RF30`–`RF34` in `selftest.ts` quote this message; the three lists are typed
2663
+ there rather than read off the tables, so widening any of the three without
2664
+ moving this page turns them red.
2665
+
2427
2666
  **A physics constraint's six tuning timelines override §4.6's table for the
2428
2667
  length of an animation.** `{ "physics": "hair", "property": "wind", "keys": […] }`
2429
2668
  is a wind that rises and falls; `damping` is how fast the jiggle settles,
@@ -3999,11 +4238,17 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3999
4238
  | `rig constraint "X": declares "property" but no "bone"` | §3.5.2 — name the driving bone, or key `slider.<name>.time` instead |
4000
4239
  | `rig constraint "X": drives off bone "Y" rotate with "local": false, and the driving values that reach animation "A" (0s..Ds) run from −15.000° to 15.000° … the whole part of the range below 0° is dead` | §3.5.2 — add `"local": true`, which reads the bone's own rotation signed and unwrapped, or move the range so it does not cross 0°. A world rotation is wrapped into `[0, 360]` before the slider maps it, so the negative half of the range is unreachable and pins to one frame |
4001
4240
  | `… run from 300.000° to 500.000° … the whole part of the range past 360° is dead` | §3.5.2 — the same wall at the other end, and the same first repair: `"local": true`, or move the range so it does not run past 360°. `[0, 360]` is the whole of what that reader returns, so a bone turned to 500° is read as 140° and selects a time far from the one the range asked for. Ending *exactly* on 360° is fine — that is the full turn, and it misses nothing: the wrap rounds, so a bone a hair below 0° is read as exactly 360 |
4241
+ | `rig constraint "X": drives off bone "Y" scaleX with "local": false, … run from -2.000 to 2.000 … Positions below 0 read as the mirror of positions above it: 2.000 of the range below 0 repeats 0.000..2.000` | §3.5.2 — a world scale reading is `Math.sqrt(a² + c²)`, a magnitude, so the half of the range below 0 is the half above it played backwards. Add `"local": true`, which reads `source.scaleX` signed, or move the range so it does not dip below 0. A bottom of exactly 0 is fine — that floor is reached, not approached |
4242
+ | `… run from -6.000 to -2.000 … the whole 4.000 of this range is below 0, so it reaches none of the animation's 1s` | §3.5.2 — the same floor with the range wholly under it: every value the reader can return maps past the animation, so the dial holds one frame at every position. The figure is the width below 0, not the reach to the far end |
4002
4243
  | `skin "S" activates bone "B", but that bone does not declare \`"skin": true\`` | §3.4.1 — the list and the flag are one switch; add the flag or drop the list |
4003
4244
  | `bone "B" declares \`"skin": true\` but no skin activates it` | §3.4.1 — the other half: list it in the skin it belongs to, or drop the flag |
4004
4245
  | `skin "S": uses the long form … and also has a key "X"` | §3.4.1 — move the slot inside `attachments` |
4005
4246
  | `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 |
4006
4247
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
4248
+ | `rgba value needs 4 channels, got 3` | §4.4 — an `rgba` key is `[r, g, b, a]`. It names no animation, slot or key time, and the only input that reaches it is a slot `rgba` key: the setup pose's `color` is refused earlier, by its own row, with the slot named |
4249
+ | `animation "A" bone "B" has no timeline "P" (it has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate)` | §4.4 — a bone has exactly ten timelines and `P` is none of them. Fix the spelling — the single-axis ones are lower-case (`translatex`, not `translateX`). A **constraint** property is refused first, by its own row, naming the field its constraint's name goes in. When `P` is a slot timeline the message says so and where to put the name: `. "rgba" is a slot timeline — put the name in "slot"`. Before [#656](https://github.com/firejune/rigc/issues/656) all of them read `bone "B" cannot take slot property "P"`, which named the slot family whatever you had written and listed nothing |
4250
+ | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate; a slot group has: attachment, rgba; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | §4.3, §4.4 — a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one. Before [#661](https://github.com/firejune/rigc/issues/661) a group of bones read `animation "A" targets unknown slot "M"` and a group of slots got the slot row below, naming one family out of three |
4251
+ | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba)` | §4.4 — a slot has exactly two timelines and `P` is neither. Fix the spelling; a bone or constraint property written on a slot track is refused by its own row instead. Before [#650](https://github.com/firejune/rigc/issues/650) every other name compiled as an **rgba** timeline called `P`, and what you saw was `A00_ROUNDTRIP_PARSE` on the emitted file — or, for the one-channel spelling, `rgba value needs 4 channels, got 1` |
4007
4252
  | `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)) |
4008
4253
  | `N pair(s) of skin names have no one order: … "Zulu" / "mike" (case) — folded to one case "Zulu" and "mike" order the other way round, so whether the editor folds SKIN names decides this pair` | **R11** — rename until no pair is left. The same shape as the row above with a **wider** family: #539 measured the editor's comparator for animation names and thereby ruled codepoint out, and nothing has ruled anything out for skin names, so a pair the candidates could disagree about is refused even where the animation rule would emit it. `Zulu`/`mike` and `mike10`/`mike2` build as animation names and are refused as skin names ([#541](https://github.com/firejune/rigc/issues/541)) |
4009
4254
  | `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that … Move the default skin's entry for this slot into a named skin — call it "base"` | **R12** — do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and §3.4.2 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
@@ -4109,8 +4354,9 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
4109
4354
  | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` (§3.5.2). **SKIP** when the skeleton declares no slider |
4110
4355
  | `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` |
4111
4356
  | `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. ⚠️ And the **skin** it poses in is the one the timeline is keyed on (§4.11.5, issue #583), since a deform's address is a `skin / slot / attachment` triple: the pose wears that skin, which also switches on any `skin: true` bone or constraint it activates, and the "nothing is drawn" sentence names the skin it was read under. **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 |
4112
- | `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 |
4357
+ | `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 applied additively (a slot colour, an attachment swap, a draw order, an ik mix, a path's `spacing`, most physics properties), where `"additive": true` is not the fix and one of the two has to go. ⭐ Which of the two it is, is **posed rather than read off `Timeline.additive`**: the shared timeline is applied twice with `add` set and the detail says what it did ([#655](https://github.com/firejune/rigc/issues/655) — two classes declare that flag falsely about themselves, so a path constraint's `mix` and a slider's `time` were refused although they compose). The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, which one wins today, and the class that was posed. Four 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, two sliders on different properties, and a shared timeline that writes **nothing a pose holds** — an `events` timeline fires no event under a slider (`firedEvents` is null), so neither slider has anything there for the other to erase. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
4113
4358
  | `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 |
4359
+ | `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` | both | a slider whose animation keys a property of a constraint **at or before it** in `constraints` (§3.5.2) — a slider's `mix` or `time`, an ik or transform mix, a path `position`, `spacing` or `mix`, any physics value. That array is the update order for every kind, and each constraint reads its own applied pose when its turn comes — `Slider.update` takes `mix` as the alpha it applies with and `time` as the time it applies at, `PhysicsConstraint.update` returns on `mix` 0 before reading the rest — so the key lands after the only read of it and `Posed.resetConstrained` discards it before the next frame: what the driven constraint drives is dead at every position of the driving dial, although its pose still holds the number ([#658](https://github.com/firejune/rigc/issues/658), [#665](https://github.com/firejune/rigc/issues/665)). The detail names the slider, the driven constraint with its kind, both array indices, the property, the runtime class whose `update` reads it, and the animation the key sits in. Fix by moving the driver earlier, or by keying that property from a slider that already is. **The two indices equal is the same failure**: a slider cannot key its own `mix` or `time`, and one muted at setup that keys its own `mix` up never applies anything at all — `A37` is silent there, because it asks whether *an* animation keys the mix and not which one. **Two shapes it deliberately leaves out**, both measured: a `physics` `reset` key, which fires on a crossed frame time and so never fires from a slider at all, in either order — the reorder would repair nothing; and a physics timeline naming no constraint, which is every physics constraint declaring that property global and IS refused for the ones already run. Disjoint from `A40` by construction: `A40` asks who writes a shared property last and excludes every slider whose `mix` is keyed, this asks whether anything reads what was written. **SKIP** when the skeleton declares no slider, and when no slider's animation keys a constraint property — that SKIP names any `reset` keys it found — a pass means a driver and a driven were compared |
4114
4360
 
4115
4361
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
4116
4362
  clauses are gated by profile.
package/docs/FACE.md CHANGED
@@ -1205,6 +1205,133 @@ green. ⇒ write `"additive": true` on **every** slider that shares a target and
1205
1205
  not only on the later one, which is what the paragraph above already asks for and
1206
1206
  this is the second reason for.
1207
1207
 
1208
+ 🚨 **A second dial on a slot colour, an attachment swap or a draw order is not a
1209
+ second dial at all.** Those timelines ignore `additive`, so the flag is not the
1210
+ repair and writing it on both changes nothing: the slider **later in the
1211
+ `constraints` array** owns that property outright and the earlier one contributes
1212
+ nothing, at every position of its dial. An **ik constraint's mix** behaves the
1213
+ same way. What does compose is the rest of what a face keys — a bone transform, a
1214
+ mesh deform, a transform constraint's mix, a physics `wind`, a path constraint's
1215
+ `mix`, and another slider's own `mix` or `time` — and those are the same sum as
1216
+ the two axes above, over each target's own setup value. ⇒ if a blink fades a slot
1217
+ and the yaw dial also fades it, one of the two has to stop: key that property
1218
+ from **one** slider, or move both edits into the animation a single slider
1219
+ applies. `A40` refuses the rest by name, and AUTHORING §3.5.2 is the mechanism.
1220
+
1221
+ ✅ **And which of the two a timeline is, the gate now poses rather than asks.**
1222
+ It read `Timeline.additive`, the runtime's own declaration, which two classes
1223
+ state falsely about themselves — so a path constraint's `mix` and a slider's
1224
+ `time`, both in the composing list above, were refused by name with a message
1225
+ saying the flag would not compose them
1226
+ ([#655](https://github.com/firejune/rigc/issues/655)). `A40` applies the shared
1227
+ timeline twice with `add` set instead and reads whether the second application
1228
+ accumulated. The same measurement retired a refusal no pose could have told
1229
+ apart: two dials whose animations both fire **events** were refused, and a slider
1230
+ fires no event at all — it applies its animation with `firedEvents` null.
1231
+
1232
+ ⭐ **Three dials are the same sum as two — as long as every one of them is
1233
+ additive.** The case worth knowing is a non-additive dial in the *middle* of
1234
+ three, because it is neither of the two failures you would expect: it erases every
1235
+ dial **before** it and is then added to by every dial **after** it, so the face is
1236
+ neither the sum nor the last dial alone, and no reading of a two-dial rig has that
1237
+ shape.
1238
+
1239
+ ⚠️ **`"loop": true` puts the animation's FIRST frame at the top of the dial.** A
1240
+ looping slider wraps its time as a positive modulo rather than holding the last
1241
+ frame, so the axis is a sawtooth: the two ends of the range are the same pose and
1242
+ every position past the top repeats the range from its bottom. That is right for a
1243
+ parameter that genuinely cycles — a wheel, a breath — and wrong for a yaw, where
1244
+ the range's top has to *stay* at the extreme of the turn. Leave `loop` off for a
1245
+ face axis; the default is the one you want.
1246
+
1247
+ 🔸 **And a `local: false` dial never reads back the number you set.** The world
1248
+ reader goes through the bone's matrix, where the reference runtime's float32 π
1249
+ leaves a fraction of a degree behind, and it has a period: a bone one whole turn
1250
+ from its position reads *identically*, so the dial cannot tell the two apart. The
1251
+ composition is unchanged — two world dials add exactly as two local ones do — but
1252
+ `local: true` is what makes the number on the dial the number the rig reads, which
1253
+ is the same repair §3.5.2's circle already asks for. Every one of the six
1254
+ `property` readings composes by that one arithmetic under `local: true`, and so
1255
+ does every one of them read through the world — the composition is the same sum
1256
+ on both sides of the flag, and what the flag changes is what the dial can say.
1257
+
1258
+ 🚨 **A `local: false` scale axis folds at zero: the negative half is the positive
1259
+ half again.** A world scale reading is a square root, so a dial at −2 and a dial
1260
+ at +2 are not two positions — they read the same, select the same frame and pose
1261
+ the same face. Nothing at compile or at runtime says so, and a squash axis
1262
+ authored through negative scale therefore gets the mirror of the dial you wrote,
1263
+ symmetric about the point where it should have passed through. A world `shearY`
1264
+ axis folds the same way at a whole turn, and its seam is not at a fixed value —
1265
+ it moves with wherever the bone is pointing. ⇒ for a scale or a shear axis, write
1266
+ `local: true`; for a scale axis you cannot, keep the whole range on one side of
1267
+ zero, because the reader has no other half to give you.
1268
+
1269
+ ⭐ **A dial can drive another dial's authority, and that composes as a product.**
1270
+ A slider's own `mix` is a keyable property, so one axis can scale another axis'
1271
+ whole contribution — a "strength" dial over an expression, which is the one shape
1272
+ on this page that multiplies rather than adds. ⚠️ **It only works downward through
1273
+ the `constraints` array.** A slider reads its own authority when its turn comes
1274
+ and the array is the update order, so a dial that keys the `mix` of a slider
1275
+ *earlier* than itself writes a number that slider has already read past: the
1276
+ driven axis is dead at every position, every frame. ✅ **The gate names that pair
1277
+ now** — `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` refuses it with both
1278
+ sliders, the property and both array indices, and says which way to move them
1279
+ ([#658](https://github.com/firejune/rigc/issues/658)). Until then it was green,
1280
+ for a reason worth knowing: a slider whose `mix` is keyed at all leaves `A40`'s
1281
+ comparison, so nothing in the tool was looking at that pair. ⇒ put the driving
1282
+ slider **first**, which is what the refusal tells you to do.
1283
+
1284
+ 🚨 **And a dial cannot turn itself on.** `Slider.update` reads its own `mix` as
1285
+ the alpha it applies the animation with, before that animation runs, so a slider
1286
+ whose *own* animation keys its `mix` writes after the only read of it — the same
1287
+ refusal with the two array indices equal. The shape to watch for is an axis
1288
+ muted at setup that means to raise itself: it never applies anything at all,
1289
+ because `update` returns on `mix` 0 before reaching the key that would raise it,
1290
+ and `A37` reports green because it asks whether *an* animation keys the mix and
1291
+ never which one.
1292
+
1293
+ 🚨 **And it is not only another dial: a face axis that drives a jiggle or a
1294
+ path is the same rule.** `physics.<name>.wind`, `path.<name>.position`, an ik or
1295
+ transform mix — every property a dial can key belongs to a constraint that reads
1296
+ it when its own turn comes, and that turn is its place in `constraints`. A
1297
+ "wind strength" dial declared after the physics constraint it drives poses the
1298
+ number in that constraint's pose and moves nothing at all: [measured] the bone a
1299
+ physics constraint drives travels `0.000e+0` across the dial with the constraint
1300
+ declared first and `4.256e+2` with it declared last, and a path constraint's
1301
+ rider `0.000e+0` against `1.620e+2`
1302
+ ([#665](https://github.com/firejune/rigc/issues/665)). ✅ `A42` names those pairs
1303
+ too, with the constraint's kind and both array indices. ⇒ **every constraint a
1304
+ dial drives goes after that dial in `constraints`** — which, for a face, means
1305
+ the dials come first and the jiggles, paths and aim constraints they scale come
1306
+ after. 🔸 One key is outside the rule because no order repairs it: a `physics`
1307
+ `reset` from a dial fires on a crossed frame time and a slider applies its
1308
+ animation at a single instant, so it never fires at all — the gate says that in
1309
+ its SKIP rather than asking you to move anything.
1310
+
1311
+ 🔸 **The bone-less slider is the same story one field over.** A slider with no
1312
+ `bone` takes its time from `slider.<name>.time`, which any animation can key — and
1313
+ two dials keying it *add*, so a time-driven axis composes like everything else
1314
+ here, and since [#655](https://github.com/firejune/rigc/issues/655) the gate
1315
+ agrees: it used to refuse exactly this rig. The same array rule applies, for the
1316
+ same reason — and `A42` refuses it there too, naming `time` instead of `mix`.
1317
+ ⚠️ Two things the bone
1318
+ form does and this one does not: there is no `Math.max(0, time)` and no wrap, so a
1319
+ driven time below zero does not pose the first frame — it leaves the pose exactly
1320
+ as it found it, which is a different picture whenever the animation's first frame
1321
+ is not the rest pose.
1322
+
1323
+ ⚠️ **Two `skinRequired` sliders are three states, not two.** Under a skin that
1324
+ lists one of them the face is that dial alone; under a skin that lists the other
1325
+ it is the other alone; and under a skin that lists **neither** — the default skin
1326
+ is usually one — every dial is dead and the face holds its rest pose with both
1327
+ dials turned to their extremes. That last state is indistinguishable, from the
1328
+ outside, from a rig whose sliders do not work, and the gate is right to be silent
1329
+ about it because the pair genuinely never meets.
1330
+
1331
+ ⭐ **Four dials are the same sum as three.** Nothing new arrives with the fourth
1332
+ axis: the arithmetic, the flag, and the non-additive-in-the-middle case all read
1333
+ exactly as they do above. Write `"additive": true` on all of them.
1334
+
1208
1335
  ✅ **The editor half, measured.** This paragraph said *unknown* until the round
1209
1336
  trip was taken with `tools/editor_roundtrip.ts` on a licensed editor (data
1210
1337
  version 4.3.26) against a 4.3.13 build of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
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": {