spine-rigc 0.25.6 → 0.26.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
@@ -533,9 +533,9 @@ first three work on any reference you have, and `bench` is a repository workflow
533
533
  and `bun run fetch-examples`. The reasoning behind them is in
534
534
  [the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
535
535
 
536
- `build` and `validate` both default to `--profile spine` — the 28 validity rules, which
536
+ `build` and `validate` both default to `--profile spine` — the 29 validity rules, which
537
537
  ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
538
- adds all 43: the other 15 are one renderer's policy and one canvas budget's, and they
538
+ adds all 44: the other 15 are one renderer's policy and one canvas budget's, and they
539
539
  fire on perfectly correct editor-produced Spine data, which is why they are opt-in.
540
540
  ⇒ **That reason is about foreign data and does not carry to a rig you are authoring
541
541
  yourself: author under `--profile spine-html` and read the extra 15 as findings, and
@@ -587,7 +587,7 @@ what each is worth.
587
587
  **What it reads is skeleton JSON and nothing else** — no `.spine` project, no binary
588
588
  `.skel`, no atlas, no art. So it never invents, and the things it cannot get out of
589
589
  the file are **findings** with codes rather than plausible values: a construct the
590
- spec format cannot hold (`linkedmesh`, `point`, a `sequence` block, an unknown field
590
+ spec format cannot hold (`point`, a `sequence` block, an unknown field
591
591
  on a bone, slot or constraint) is a blocker, the command exits non-zero, and both
592
592
  specs are still written — a spec plus a list of what is missing from it beats no spec.
593
593
  One thing it drops on purpose and says so: a path attachment's `lengths`, which is
@@ -681,7 +681,7 @@ letting `A17` blame the editor for the harness's own doing.
681
681
  | 📥 **[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 |
682
682
  | 🤖 **[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 |
683
683
  | 🔬 **[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 |
684
- | 🎓 **[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 |
684
+ | 🎓 **[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 44 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 |
685
685
  | 📋 [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 |
686
686
  | 🗺️ [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 |
687
687
  | 📐 [CLAUDE.md](https://github.com/firejune/rigc/blob/main/CLAUDE.md) | **the doctrine** — why the validator's messages are the product, why nothing reaches disk before green, why no number is ever invented, and what a change has to keep. [CONTRIBUTING.md](https://github.com/firejune/rigc/blob/main/CONTRIBUTING.md) calls it worth ten minutes before a first patch. Repository material |
@@ -738,7 +738,7 @@ quality."* All six, with their verdicts, are in
738
738
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
739
739
 
740
740
  The whole dossier — the yardstick, `diff` and `check` and what neither of them can
741
- see, every rung, the run viewer, the 43 assertions and the selftest behind them — is
741
+ see, every rung, the run viewer, the 44 assertions and the selftest behind them — is
742
742
  [docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
743
743
  Live rung status is
744
744
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
package/cli.ts CHANGED
@@ -67,7 +67,7 @@ import {
67
67
  type DeformSpan,
68
68
  } from './src/deformmeasure.ts';
69
69
  import { diffLines, diffSkeletons, reportedFigures, sectionFigures, type DiffReport } from './src/diff.ts';
70
- import { ingest, IngestError, INGEST_GUTTERS, type IngestStage } from './src/ingest.ts';
70
+ import { ingest, IngestError, IngestSpecRefused, INGEST_GUTTERS, type IngestFinding, type IngestStage } from './src/ingest.ts';
71
71
  import { copyAtlasPages } from './src/emit.ts';
72
72
  import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, packAtlas, parseAtlasText } from './src/atlas.ts';
73
73
  import { parseJsonWithPosition } from './src/json-position.ts';
@@ -2923,14 +2923,30 @@ function cmdIngest(flags: Record<string, string>, positional: string[]): void {
2923
2923
  console.log(` .. art ${art}`);
2924
2924
  if (specImages !== undefined) console.log(` .. images ${specImages} (the rig spec's own, from ${outDir})`);
2925
2925
 
2926
- const result = ingest(readJsonFile(skeletonPath), {
2927
- name: flags.name ?? basename(skeletonPath, '.json'),
2928
- art,
2929
- images: specImages,
2930
- stage,
2931
- source: basename(skeletonPath),
2932
- version: readVersion(),
2933
- });
2926
+ /**
2927
+ * What the run has to report, however the parse went.
2928
+ *
2929
+ * 🔒 A spec the tree's own parser refuses is a **finding**, not an escape
2930
+ * (issue #692): the three files are written, the coded `BLOCK` line is
2931
+ * printed, and the exit code comes off the findings like every other run's.
2932
+ * The two specs are read as `unknown` because that is all this function does
2933
+ * with them — `JSON.stringify` — and a cast to `RigSpec` here would be this
2934
+ * file claiming a parse that did not happen.
2935
+ */
2936
+ let result: { rig: unknown; motion: unknown; findings: IngestFinding[] };
2937
+ try {
2938
+ result = ingest(readJsonFile(skeletonPath), {
2939
+ name: flags.name ?? basename(skeletonPath, '.json'),
2940
+ art,
2941
+ images: specImages,
2942
+ stage,
2943
+ source: basename(skeletonPath),
2944
+ version: readVersion(),
2945
+ });
2946
+ } catch (err) {
2947
+ if (!(err instanceof IngestSpecRefused)) throw err;
2948
+ result = { rig: err.rig, motion: err.motion, findings: err.findings };
2949
+ }
2934
2950
 
2935
2951
  mkdirSync(outDir, { recursive: true });
2936
2952
  // Indent 2, which is what `compile` writes the skeleton with. One emitter
package/docs/AUTHORING.md CHANGED
@@ -169,7 +169,7 @@ What the flags mean:
169
169
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
170
170
  | `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
171
171
  | `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
172
- | `--profile` | `spine` = the 28 validity rules (**the default**) · `spine-html` = all 43, opt-in |
172
+ | `--profile` | `spine` = the 29 validity rules (**the default**) · `spine-html` = all 44, opt-in |
173
173
  | `--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 |
174
174
  | `--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 |
175
175
  | `--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` |
@@ -208,10 +208,15 @@ part per page" flat and rigc's own pack could not satisfy it. Since
208
208
  [#266](https://github.com/firejune/rigc/issues/266) that clause is **one part per
209
209
  page OR a tiling page**, so the combination is an ordinary build — and it is the
210
210
  only one that puts the renderer's own rulebook over shared-page sampling. What a
211
- *tiling* page has to satisfy is stated where the clause is, §7's `A06` row: every
212
- region wholly inside the page it names, and no two regions on one page
213
- overlapping. Rotation is still refused, and that is a separate clause about
214
- rigc's packer never turning a region.
211
+ *tiling* page has to satisfy under that profile is stated where the clause is,
212
+ §5.2's `A06` row: no two regions on one page overlapping. Rotation is still
213
+ refused, and that is a separate clause about rigc's packer never turning a region.
214
+
215
+ ⚠️ The other half of that sentence — every region wholly inside the page it names
216
+ — left this profile in [#694](https://github.com/firejune/rigc/issues/694). It is
217
+ **validity**, so no profile switches it off: a rectangle outside its page is
218
+ broken for every consumer, while two regions over the same texels is something
219
+ correct, editor-exported data does.
215
220
 
216
221
  ### 0.1 Packing the parts onto shared pages — `--pack`
217
222
 
@@ -345,7 +350,7 @@ Four things are refused rather than warned about, because each of them otherwise
345
350
  | a region name the atlas does not have | `AtlasAttachmentLoader` returns null and the part silently does not draw. The refusal lists the near misses — the usual cause is one character |
346
351
  | a size the spec disagrees with | the same silence `A06` exists for, one link earlier: a quad sized against a region of another size collapses |
347
352
  | a page the atlas names and the disk lacks | nothing to sample; caught on the way in, so the message names the atlas rather than the artifact rigc wrote from it |
348
- | a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does |
353
+ | a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does. The gate names the same rectangle, under every profile, for a pack that reaches it without passing through here — `A06`, §5.2 ([#694](https://github.com/firejune/rigc/issues/694)) |
349
354
 
350
355
  One limit, stated rather than discovered:
351
356
 
@@ -491,7 +496,7 @@ the first:
491
496
 
492
497
  | gutter | meaning |
493
498
  | --- | --- |
494
- | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `linkedmesh`, `point`, an attachment `sequence`, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
499
+ | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `point`, an attachment `sequence`, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
495
500
  | `JUDGE` | the skeleton cannot answer and somebody has to: the stage, and each animation's duration |
496
501
  | `LOSS` | the skeleton's spelling and rigc's differ, on purpose, and the line says how. A path attachment's `lengths` is the one that matters — it is `PathConstraint`'s own four-sample measurement rather than an arc length (#560), so a transcribed one would freeze whatever produced the source. The header ones are cheaper: `HEADER_BOOKKEEPING` for a field the spec has no home for, `HEADER_REDERIVED` for the version string, `HEADER_ORIGIN` for an origin the source left to the format and the rebuild writes out (#622) |
497
502
 
@@ -944,7 +949,7 @@ and the inheritance silently falls back to Normal — assertion `A02` refuses it
944
949
  | `bone` | required; must be a bone this rig declares | — |
945
950
  | `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) — **except** on a slot nothing fills, where it can only be `null` and may be left out |
946
951
  | `color` | `rrggbbaa` tint | opaque white |
947
- | `dark` | two-colour tint, `rrggbb` | — (🚫 `A12` under `spine-html`) |
952
+ | `dark` | two-colour tint, `rrggbb`. The **setup** half; §4.4's `rgba2` track keys it over time and requires it | — (🚫 `A12` under `spine-html`) |
948
953
  | `blend` | `normal` · `additive` · `multiply` · `screen` | `normal` |
949
954
 
950
955
  ✅ **Every slot you declare is emitted, in this order.** A slot nothing fills — no
@@ -1186,6 +1191,60 @@ rather than the figure it was filed over:
1186
1191
  README carries the inradius arithmetic, both coverage readings, and the rim move
1187
1192
  that settled it.
1188
1193
 
1194
+ **Linked mesh** ([Spine: linked meshes](http://esotericsoftware.com/spine-meshes)) —
1195
+ a mesh that draws **another mesh's geometry** with **its own art**. It is the type
1196
+ a skin variant uses: one triangulation and one set of weights, several outfits over
1197
+ it. Say `type: "linkedmesh"`, or put `source` on a `type: "mesh"` — the format has
1198
+ both spellings, they share one parser branch, and `source` is what decides between
1199
+ them (`SkeletonJson.ts:568-569`, `:582`), so rigc reads them the same way.
1200
+
1201
+ | Field | Meaning |
1202
+ | --- | --- |
1203
+ | `source` | **required.** The **placeholder** of the mesh whose geometry this one draws — the key it is filed under in its skin, not its `name`. A miss is refused naming the skin, the slot and what that slot holds |
1204
+ | `slot` | the slot the source lives in. Default: **this attachment's own slot**. Resolved by name against the rig's slots |
1205
+ | `skin` | the skin the source lives in. Default: **`default`** — the default skin, *not* the skin this link is written in. Resolved by name |
1206
+ | `timelines` | default **`true`**: the link plays the source's `deform` keys. `false` makes it its own timeline target, so only keys written against the link move it |
1207
+ | `image`, `path`, `width`, `height`, `color` | exactly as on a mesh — the link resolves **its own** region, which is the point of the type |
1208
+
1209
+ ```json
1210
+ "skins": {
1211
+ "base": { "cloak": { "cloak": { "type": "mesh", "image": "cloak_red.png", "uvs": [], "triangles": [], "weights": [] } } },
1212
+ "winter": { "cloak": { "cloak": { "type": "linkedmesh", "image": "cloak_blue.png", "source": "cloak", "skin": "base" } } }
1213
+ }
1214
+ ```
1215
+
1216
+ 🚨 **A linked mesh states no geometry of its own, and every geometry key on one is
1217
+ refused by name.** `uvs`, `triangles`, `vertices`, `weights`, `boneIndexing`,
1218
+ `hull`, `edges` and `generator` are read by **nothing**: the parser returns from
1219
+ the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`). Measured on
1220
+ a forged skeleton — a link declaring 5 uvs, 3 triangles, `hull: 5` and
1221
+ `edges: [0, 2]` beside a 4-vertex source loaded with the **source's** 8-long
1222
+ `worldVerticesLength`, 6 triangles, `hullLength` 8 and 10 edges. Nothing the author
1223
+ wrote reached anything and nothing said so.
1224
+
1225
+ 🚫 **A chain is refused, and so is a link to itself.** A `source` that names
1226
+ another linked mesh resolves in the order the file was read: measured through
1227
+ spine-core, the chained link loaded the full geometry with the source declared
1228
+ first, and `worldVerticesLength` **0**, 0 triangles and a 0x0 size with the two
1229
+ keys swapped in the same file — silently, both ways. A construct whose meaning
1230
+ depends on JSON key order is one rigc will not write. Point `source` at the mesh.
1231
+
1232
+ ⚠️ **`width`/`height` are emitted and the gate cannot see them.** The runtime
1233
+ overwrites both with the source's when it resolves the link
1234
+ (`MeshAttachment.setSourceMesh`; measured: a link stating `99x77` beside a 32x32
1235
+ source loads as 32x32). They are written because the editor reads them off the
1236
+ file and because the spec stated them — R1 — and no assertion can check them.
1237
+
1238
+ 🔸 **`A21_MESH_RIM_PINNED` and `A28_RIBBON_ROWS_SHARE_WEIGHTS` leave a link out**,
1239
+ for the same reason they leave authored geometry out: the rim and the rows it draws
1240
+ are its source's and are measured there. `A21` drops it from the set it measures and
1241
+ **SKIPs by name** — naming the link and its source — when that leaves nothing;
1242
+ `A28` passes over it. `A04`, `A20` and `A22` read a link exactly as they read any
1243
+ other mesh, because after the round trip it **is** the source's triangles, weights
1244
+ and uvs. `A13_MESH_BUDGET` counts it as a mesh of its own: the runtime draws it as
1245
+ one, so a link in a second slot is a second mesh slot against
1246
+ `invariants.meshSlots`.
1247
+
1189
1248
  The generators are `ring`, `ribbon`, `contour` and `grid` (see
1190
1249
  [`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
1191
1250
  than a table of numbers, which is why they are code invoked by data. The last two
@@ -1968,6 +2027,20 @@ rigc emits all five: `ik`
1968
2027
  and `slider`. Field lists are in [`src/rig.ts`](../src/rig.ts); the traps worth
1969
2028
  carrying here:
1970
2029
 
2030
+ - 🔑 **A constraint name is unique per KIND, not across the array.** Spine
2031
+ resolves one with `SkeletonData.findConstraint(name, type)`, which tests
2032
+ `constraint instanceof type` **before** it compares the name, and every
2033
+ resolution in the format goes through it: a timeline group, a skin's member
2034
+ list (§3.4.1), a slider's animation. So an `ik` constraint and a `transform`
2035
+ constraint may both be called `leg` — the editor exports both, the runtime
2036
+ finds each from its own group, and a motion spec's `ik` block, `transform`
2037
+ block and `path`/`physics`/`slider` tracks each name the kind they mean and
2038
+ resolve the same way (§4.9, §4.12). Two constraints **of one kind** sharing a
2039
+ name are refused (§5.1), because no timeline could say which was meant. Until
2040
+ [#692](https://github.com/firejune/rigc/issues/692) the rig spec kept one
2041
+ namespace over the whole array, so a rig the editor exports and the runtime
2042
+ plays — an IK chain and the transform constraint that follows it, both carrying
2043
+ the chain's name — could not be written down at all.
1971
2044
  - A transform constraint's `properties` names come from a fixed six — `rotate`,
1972
2045
  `x`, `y`, `scaleX`, `scaleY`, `shearY`. rigc refuses anything else by name; in
1973
2046
  raw JSON the parser throws.
@@ -2759,6 +2832,7 @@ a deform). Folding them in would make `v` mean four different things depending o
2759
2832
  | `bone` | `translate`, `scale`, `shear` | `[x, y]` |
2760
2833
  | `bone` | `translatex`, `translatey`, `scalex`, `scaley`, `shearx`, `sheary`, `rotate` | `[value]` |
2761
2834
  | `slot` | `rgba` | `[r, g, b, a]` in 0..1 |
2835
+ | `slot` | `rgba2` | `[lr, lg, lb, la, dr, dg, db]` in 0..1 — the two-colour tint, light then dark, **seven** channels. The slot must declare a setup `dark` (§3.3) |
2762
2836
  | `slot` | `attachment` | the attachment name, or `null` for "show nothing" |
2763
2837
  | `physics` | `inertia`, `strength`, `damping`, `mass`, `wind`, `gravity` | `[value]` — the constraint's own tuning, keyed over time |
2764
2838
  | `physics` | `mix` | `[mix]`, **0 or more** — the constraint's authority |
@@ -2802,9 +2876,9 @@ accepts is what it accepts.
2802
2876
  it actually poses, both ways, which is what makes the list checkable at all: it
2803
2877
  was stated there too until the refusal had something to state.
2804
2878
 
2805
- ⚠️ **A `slot` track's `property` is one of the two above, and anything else is a
2879
+ ⚠️ **A `slot` track's `property` is one of the three above, and anything else is a
2806
2880
  compile error** — `animation "A" slot "X" has no timeline "P" (it has:
2807
- attachment, rgba)`, §5.1's row. Until
2881
+ attachment, rgba, rgba2)`, §5.1's row. Until
2808
2882
  [#650](https://github.com/firejune/rigc/issues/650) it was not: the emitter had a
2809
2883
  branch for `attachment` and wrote **everything else** as an rgba timeline under
2810
2884
  the name you gave it, so a track spelled `sequence` compiled, emitted
@@ -2813,21 +2887,33 @@ the name you gave it, so a track spelled `sequence` compiled, emitted
2813
2887
  one-channel spelling of the same mistake was refused at compile as `rgba value
2814
2888
  needs 4 channels, got 1`, a message about a key you had not written.
2815
2889
 
2816
- - The pair is the emitter's own dispatch table (`SLOT_TRACKS` in
2890
+ - The three are the emitter's own dispatch table (`SLOT_TRACKS` in
2817
2891
  `src/compile.ts`): `compileTrack` reads it to pick its branch, and the refusal
2818
2892
  prints `Object.keys` of the same object, so what you are told a slot accepts
2819
2893
  is what it accepts.
2820
- - **Nothing derives this page's copy of that pair from the table**, and the list
2821
- is two names long: no `DQ*`/`RD*`/`CUR*` control reads §4.4 (the only gated
2894
+ - **Nothing derives this page's copy of that list from the table**, and it is
2895
+ three names long: no `DQ*`/`RD*`/`CUR*` control reads §4.4 (the only gated
2822
2896
  table on this page is §3.5.2.1's, held by `RD01`–`RD06`). What keeps the two
2823
2897
  in step is the control that quotes the message — `RF23` in `selftest.ts` —
2824
- which goes red if the accepted pair ever widens without this page moving with
2825
- it.
2826
- - The format has four more slot timelines (`rgb`, `alpha`, `rgba2`, `rgb2`) and
2827
- rigc emits none of them, so their names are refused here too; `A12_NO_DARK_COLOR`
2828
- refuses the last two in a file rigc did not write (SPEC_COVERAGE §2.1).
2829
- `sequence` is a timeline on an **attachment**, not on a slot, and rigc does
2830
- not emit that either.
2898
+ which goes red if the accepted list ever widens without this page moving with
2899
+ it. It did, on the day `rgba2` was added
2900
+ ([#690](https://github.com/firejune/rigc/issues/690)), which is the mechanism
2901
+ working rather than a hole in it.
2902
+ - 🎨 **`rgba2` keys the two-colour tint, and the slot has to own one first.** A
2903
+ track `{ "slot": "X", "property": "rgba2" }` on a slot whose rig spec declares
2904
+ no `dark` (§3.3) is a compile error with the slot named, and it is not a
2905
+ formality: the runtime allocates a slot's dark colour only when its setup pose
2906
+ has one, so a file keying it without one parses cleanly and then throws in the
2907
+ player the first time the animation is applied. `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN`
2908
+ (§5.2) holds the same pairing on a skeleton rigc did not write. 🚫 Both halves
2909
+ of the two-colour tint are refused under `--profile spine-html`, whose renderer
2910
+ ignores them; the default `spine` profile that `build` runs reports `A12` as
2911
+ `PROF` and never applies it.
2912
+ - The format has three more slot timelines (`rgb`, `alpha`, `rgb2`) and rigc
2913
+ emits none of them, so their names are refused here too; `A12_NO_DARK_COLOR`
2914
+ refuses `rgb2` — and `rgba2`, and the slot field — in a file under that
2915
+ renderer's profile (SPEC_COVERAGE §2.1). `sequence` is a timeline on an
2916
+ **attachment**, not on a slot, and rigc does not emit that either.
2831
2917
 
2832
2918
  ⚠️ **A `group` track's `property` is one of those two lists or the physics one,
2833
2919
  and anything else is a compile error** — `animation "A" group "G" has no timeline
@@ -2845,7 +2931,7 @@ resolved against the rig.
2845
2931
  with no table claiming the property the track fell through to the slot branch,
2846
2932
  and what you were told was that the first member is not a slot — on a file that
2847
2933
  named neither a slot nor that member. A group of **slots** got §4.4's slot row
2848
- instead (`slot "M" has no timeline "P" (it has: attachment, rgba)`), which is
2934
+ instead (`slot "M" has no timeline "P" (it has: attachment, rgba, rgba2)`), which is
2849
2935
  true of the member and names one family out of three on a track whose family
2850
2936
  nothing had determined.
2851
2937
  - **A constraint property never reaches it.** `position`, `spacing` and `time` are
@@ -3508,6 +3594,35 @@ Per key:
3508
3594
  | `offset` | the same start as a raw index into the deform array. Never with `fromVertex` |
3509
3595
  | `ease` / `curve` | one channel, and it eases the **blend**, not a coordinate |
3510
3596
 
3597
+ **The target is any attachment that has a vertex array** — a mesh, a bounding
3598
+ box, a clipping polygon, or a **path**. The array a key edits is that
3599
+ attachment's own, so everything below about runs, start indices and the two
3600
+ encodings reads the same whichever it is. A region attachment has no vertex array
3601
+ and is refused by name.
3602
+
3603
+ ⭐ **A path's vertices are its control points** — knots and their Bezier handles
3604
+ alike, in the order §3.5.1 lists them — so a run covers them in that order and a
3605
+ `fromVertex` counts them the same way. Its deform array is `vertexCount * 2` long
3606
+ unweighted, and one pair per influence weighted, exactly as a mesh's is. What is
3607
+ and is not measured on one:
3608
+
3609
+ - `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` measures the run against that length, as
3610
+ it does for every other target.
3611
+ - `A39_DEFORM_KEEPS_TRIANGLE_WINDING` reports **SKIP**, naming the slot and
3612
+ saying the attachment has no triangles. A path is knots and handles; there is
3613
+ no winding to keep, and reporting a pass for a measurement that did not happen
3614
+ is the one thing an assertion here may not do.
3615
+ - **`lengths` is not re-measured, and cannot be.** It is a field of the
3616
+ attachment (§3.5.1) and the format has nowhere to put a per-key one, so the
3617
+ array every exporter writes — rigc's included — is the **setup** measurement.
3618
+ `PathConstraint.computeWorldPositions` reads it only when `constantSpeed` is
3619
+ `false`; under the parser's default, `true`, it re-measures the curve from the
3620
+ posed vertices every frame. So a path constraint follows the deformed curve as
3621
+ written, and under `constantSpeed: false` it traverses the deformed curve at
3622
+ the **setup** spacing. That is the format's behaviour rather than rigc's
3623
+ choice, which is why it is stated here instead of refused: an editor export of
3624
+ the same rig does the same thing.
3625
+
3511
3626
  Four things are worth having straight before you write one.
3512
3627
 
3513
3628
  **A key is a sparse edit, and a key with no `vertices` is the setup pose.** The
@@ -3561,6 +3676,7 @@ half of the format:
3561
3676
  | `fromVertex` on a multi-bone vertex | `"fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` |
3562
3677
  | an attachment that is not there | `slot "flat" in skin "default" has no attachment "flatt" (it has: flat)` |
3563
3678
  | a deform on a region attachment | `a deform timeline keys the vertices of an attachment, and this one is a "region"` |
3679
+ | a run past a path's control points | `this attachment's deform array is 18 long (9 vertices)` — the same bound as any other target, counted in the control points §3.5.1 declares |
3564
3680
  | `transform` beside a `vertices` run | `the key carries both a "transform" and a "vertices" run, and they are two answers to one question` |
3565
3681
  | `transform` with `fromVertex` or `offset` | `A transform is a model of the whole attachment and is evaluated over all 25 of its vertices, so it always starts at deform index 0` |
3566
3682
  | a `transform` on an attachment whose weights do not close at 1 | `this one has a vertex the arithmetic cannot place — vertex 1's 2 weights sum to 0.9000 rather than 1` |
@@ -4438,6 +4554,9 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4438
4554
  | `a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)` | §3.3 — write `[]` for a rig that draws nothing. ⚠️ These three arrive only when the key is really absent: **misspelt**, it is the unknown-key refusal above, naming what you wrote |
4439
4555
  | `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
4440
4556
  | `two bones are called "X"` | bone names are the join key; rename one |
4557
+ | `two ik constraints are called "X" — a constraint resolves by name AND type (\`SkeletonData.findConstraint\`), so names are unique PER KIND: an ik and a transform constraint may share one, two of a kind may not` | §3.5 — rename one of the two. The kind in the sentence is the pair's own, so `two transform constraints are called "X"` is the same refusal on another kind; a name shared **across** kinds is not this error and never was one to fix |
4558
+ | `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | §4.6 — the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
4559
+ | `skin "S" activates ik constraint "X", which skin "T" already activates; a constraint belongs to one skin` | §3.4.1 — a constraint runs under one skin or under all of them. The kind is in the sentence because `ik` "X" and `transform` "X" are two constraints, and each may belong to a different skin |
4441
4560
  | `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
4442
4561
  | `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 — pick one file and declare it there. A slot **nothing** fills is exempt: its setup pose can only be "show nothing" and is not asked for |
4443
4562
  | `the setup pose shows attachment "A" on slot "X", which no skin and no manifest part fills` | §3.3 — the slot is emitted empty and nothing was ever going to fill it, so `A` resolves to nothing. Give the slot an attachment (a skin entry or a manifest part), or state the setup pose as `null` |
@@ -4446,7 +4565,14 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4446
4565
  | `a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG` | §3.4 — the same rule for a mesh |
4447
4566
  | `"type" is null, which is not a name. An attachment's type is one of region, mesh, linkedmesh, … or the key is absent and reads as "region"` | §6 — **remove the key**. Absent is the format's own default; present-and-null matches no parser case and the attachment is dropped in silence |
4448
4567
  | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (…)` | §6 — a name the format does not have. Not a deferral, and not something rigc will grow: fix the spelling (`sequence` is a key on a region or a mesh, not a type) |
4449
- | `this attachment is a "linkedmesh"` / `"point"` … `rigc does not emit it yet` | §6 — a construct the format has and rigc does not write. The message says what it would carry; SPEC_COVERAGE part 1-6 is the row it reads from |
4568
+ | `this attachment is a "point" … rigc does not emit it yet` | §6 — a construct the format has and rigc does not write. The message says what it would carry; SPEC_COVERAGE part 1-6 is the row it reads from |
4569
+ | `a linked mesh needs "source" — the PLACEHOLDER of the mesh whose geometry it draws …` | §3.4 — `source` is what MAKES a mesh linked, and the parser falsy-tests it, so an absent or empty one is read as an ordinary mesh and throws on the `uvs` a link has not got |
4570
+ | `a linked mesh states "uvs", "triangles", …, and a linked mesh has no geometry of its own` | §3.4 — remove them, or remove `source` and author this as a mesh. The parser returns before `readVertices`, so those keys are read by nothing at all |
4571
+ | `"source" is "X", and skin "S" … slot "L" … holds 2: "a", "b"` | §3.4 — `source` is the PLACEHOLDER the source is filed under, not its `name`. A clause after the skin and after the slot says whether each was stated or taken from the parser's default — **the default skin** and **this attachment's own slot**, which is the pair that surprises |
4572
+ | `"slot" is "X", which the rig does not declare as a slot` / `"skin" is "X", … the rig declares no such skin` | §3.4 — a link resolves both by name. Left to the round trip these are the runtime's `Source mesh slot not found` and `Skin not found`, which name neither the attachment nor where it looked |
4573
+ | `"source" is "X", which is itself a linked mesh, and a chain of them is refused` | §3.4 — point `source` at the mesh. A chain resolves in file order and loads nothing at all in one of the two orders, silently |
4574
+ | `"source" is "X", which is a "region" attachment and not a mesh` | §3.4 — a link takes another MESH's geometry; off any other type the runtime reads `undefined` and says nothing |
4575
+ | `a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG` | §3.4 — the mesh rule, on a link. Its art is its own |
4450
4576
  | `hull N disagrees with the triangles, whose outline has K vertices (0 → …)` | §3.4 — delete `hull`, or state K |
4451
4577
  | `hull vertices must come first; vertex i is on the boundary and vertex j is not. The triangles' outline runs …: list those K vertices first, in that order, then the M interior vertices` | §3.4 — renumber the vertices: the printed walk first, then the interior |
4452
4578
  | `hull vertices must trace the outline in order; the triangles' outline runs …, so vertex a has to follow vertex b in the list, and vertex c does` | §3.4 — renumber along the printed walk |
@@ -4498,7 +4624,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4498
4624
  | `animation "A" keys "X" as an ik constraint, but the rig declares it as a "transform" constraint` | §4.9 — a timeline's target resolves by name AND type; put the entry under the right group |
4499
4625
  | `ik constraint "X": key 0 names "softness" and key 1 (t=…) does not` | §4.9 — every key is read with its own default, so state the field on every key or on none |
4500
4626
  | `ik constraint "X" (t=…): mix is 1.5, outside 0..1` | §4.9 — an IK mix is a percentage; a transform mix is unbounded |
4501
- | `deform …: the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long` | §4.11 — shorten the run or move its start; the parser would drop the tail in silence |
4627
+ | `deform …: the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long (4 vertices)` | §4.11 — shorten the run or move its start; the parser would drop the tail in silence. The count in brackets is the **target's own**: a mesh's or a path's vertices, or a weighted attachment's bone influences |
4502
4628
  | `deform …: "fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` | §4.11 — key the control bone, or write bind-space pairs and start with `offset` |
4503
4629
  | `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
4504
4630
  | `deform … (t=…): the key carries both a "transform" and a "vertices" run` | §4.11.1 — a model and a table are two answers to one question; drop one |
@@ -4525,9 +4651,11 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4525
4651
  | `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 |
4526
4652
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
4527
4653
  | `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 |
4654
+ | `rgba2 value needs 7 channels, got 6` | §4.4 — an `rgba2` key is `[lr, lg, lb, la, dr, dg, db]`: the light colour with its alpha, then the dark colour **without** one. Six is the commonest way to get it wrong, because the dark half looks like it should take an alpha too — the format has no channel for it, and neither does the runtime's `setFrame`. Like the row above it names no animation or key time; the only input that reaches it is a slot `rgba2` key |
4528
4655
  | `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 |
4529
- | `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 |
4530
- | `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` |
4656
+ | `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, rgba2; 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 |
4657
+ | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba, rgba2)` | §4.4 — a slot has exactly three timelines and `P` is none of them. 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` |
4658
+ | `animation "A" slot "X" rgba2: slot "X" declares no setup "dark", and an "rgba2" timeline poses a slot's dark colour …` | §3.3, §4.4 — the two-colour tint has a setup half and a keyed half, and the keyed half cannot exist without the other. `Slot`'s constructor allocates a dark colour only for a slot whose setup pose declares one, and `RGBA2Timeline` writes it unconditionally — so without the `dark` the file loads, and the first `state.apply` throws `TypeError: null is not an object` in the consumer's process. Give the slot the `dark` it holds at rest, or key `rgba` if only the light colour moves. Raised before the keys are read, with the slot named, for the same reason the row above is |
4531
4659
  | `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)) |
4532
4660
  | `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)) |
4533
4661
  | `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 |
@@ -4628,7 +4756,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
4628
4756
  | `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height`. **SKIP** when the skeleton carries no region attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
4629
4757
  | `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
4630
4758
  | `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel**. **SKIP** when no animation carries a timeline at all ([#580](https://github.com/firejune/rigc/issues/580)). Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one |
4631
- | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk. Under `spine-html` also: `pma`, rotation, and a page that is neither **one part covering it exactly** (the unpacked convention) nor a **tiling** — a page whose regions all sit inside it and none of which overlap ([#266](https://github.com/firejune/rigc/issues/266)). A packed atlas therefore gates under this profile; what the message names is the region that runs off its page, or the pair that shares texels. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
4759
+ | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk, **or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles ([#694](https://github.com/firejune/rigc/issues/694)): a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels — a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling** ([#266](https://github.com/firejune/rigc/issues/266)), and what that message names is the pair that shares texels. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
4632
4760
  | `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` |
4633
4761
  | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. Until [#589](https://github.com/firejune/rigc/issues/589) they were unreachable: the loader threw first and the miss arrived as `A00_ROUNDTRIP_PARSE`. There is no `spine-html` clause here any more — a placeholder is free to differ from the region its `path` names ([#574](https://github.com/firejune/rigc/issues/574)) **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once ([#580](https://github.com/firejune/rigc/issues/580)) |
4634
4762
  | `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
@@ -4665,6 +4793,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
4665
4793
  | `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 |
4666
4794
  | `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 |
4667
4795
  | `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 |
4796
+ | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (§3.3) or an `rgba2` timeline (§4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** — it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** — `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. ⚠️ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` — there is then no two-colour tint to read back |
4668
4797
 
4669
4798
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
4670
4799
  clauses are gated by profile.
@@ -4679,10 +4808,12 @@ own behaviour is worse: an unknown attachment `type` returns `null` and the
4679
4808
  attachment disappears, and a constraint entry with an unrecognised `type` matches no
4680
4809
  case and vanishes.
4681
4810
 
4682
- A deferral carries its reason, and the reason is the same one in every deferred row:
4683
- **neither of those types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
4684
- parts 3-1 and 4-2), so neither is on the ladder's critical path. The message
4685
- says so, because a deferral without its reason is a wall rather than a work item.
4811
+ A deferral carries its reason, and there is one deferred attachment type left:
4812
+ **`point` appears nowhere in the benchmark corpus** (SPEC_COVERAGE parts 3-1 and
4813
+ 4-2), so it is not on the ladder's critical path. The message says so, because a
4814
+ deferral without its reason is a wall rather than a work item. `linkedmesh` stood
4815
+ beside it until [#691](https://github.com/firejune/rigc/issues/691) and is now
4816
+ emitted — §3.4 has its fields.
4686
4817
 
4687
4818
  ⚠️ **A spelling the format does not have is a different refusal and says so.**
4688
4819
  `sequence` is not an attachment type, and a `"type"` that is `null` is not an absent
@@ -4693,13 +4824,12 @@ are `CompileError`s, and they name what the format actually defines
4693
4824
 
4694
4825
  | You wrote | You get |
4695
4826
  | --- | --- |
4696
- | attachment `type` of `point` or `linkedmesh` | `this attachment is a "linkedmesh" — a mesh that takes its geometry from another mesh instead of stating any — a region/mesh head, then "source" …. rigc does not emit it yet, deliberately: it emits region, mesh, boundingbox, clipping, path, and neither a point nor a linked mesh appears anywhere in the benchmark corpus …` — the message names the **construct**, not just its type string, and part 1-6 is where the sentence comes from |
4697
- | a mesh carrying `source` (`type: "mesh"` **or** `type: "linkedmesh"`) | the same refusal, prefixed `(a mesh carrying "source" is one)`. The two spellings share one parser branch and `source` is what decides between them (SPEC_COVERAGE part 1-6), so `source` on a mesh is a linked mesh whatever `type` says. It used to be refused as *2 keys this compiler does not read: "source", "skin" … fix the spelling or remove it*, whose remedy destroys the construct ([#577](https://github.com/firejune/rigc/issues/577)) |
4827
+ | attachment `type` of `point` | `this attachment is a "point" — a position and an angle with no geometry at all — "x", "y", "rotation" and "color" …. rigc does not emit it yet, deliberately: it emits region, mesh, linkedmesh, boundingbox, clipping, path, and a point appears nowhere in the benchmark corpus …` — the message names the **construct**, not just its type string, and part 1-6 is where the sentence comes from |
4828
+ | a mesh carrying `source` (`type: "mesh"` **or** `type: "linkedmesh"`) | **not a refusal any more** — both spellings compile to a linked mesh (§3.4, [#691](https://github.com/firejune/rigc/issues/691)). They share one parser branch and `source` is what decides between them (SPEC_COVERAGE part 1-6), so `source` on a mesh is a linked mesh whatever `type` says. It was once refused as *2 keys this compiler does not read: "source", "skin" … fix the spelling or remove it*, whose remedy destroys the construct ([#577](https://github.com/firejune/rigc/issues/577)) |
4698
4829
  | attachment `type` of anything else — `sequence`, a typo | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (region, mesh, linkedmesh, boundingbox, path, point, clipping). … the attachment is dropped from the skeleton without a word` — a **`CompileError`**, not a deferral: rigc is not going to implement a name the format does not have. (`sequence` is a key on a region or a mesh, not a type of its own.) |
4699
4830
  | `"type": null` | `"type" is null, which is not a name. … PRESENT-and-null is not absent: getValue(map, "type", "region") takes the default only when the key is missing, so this map matches no case, readAttachment returns null, and the attachment is dropped from the skeleton without a word. Remove the key, or name a type.` Leaving the key **out** is legal and reads as `region`; writing it as `null` is not the same thing ([#577](https://github.com/firejune/rigc/issues/577)) |
4700
4831
  | 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 |
4701
4832
  | 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 |
4702
- | 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 |
4703
4833
  | 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 |
4704
4834
 
4705
4835
  Two more limits that are not errors but will shape what you can attempt:
package/docs/INGEST.md CHANGED
@@ -539,7 +539,7 @@ is the one failure a comparison of two sets cannot show you.
539
539
  | code | gutter | exit | what it means | what to do |
540
540
  | --- | --- | --- | --- | --- |
541
541
  | `ANIMATION_GROUP` | `BLOCK` | 1 | the animation carries a group the motion spec has no home for. The detail names the ten it does carry. `drawOrderFolder` is the group to know about: the runtime reads it and builds a timeline from it, and no export in this corpus carries one | transcribe that group by hand (§2), or accept that the rebuild does not carry it |
542
- | `ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so it reads `ATTACHMENT_POINT` or `ATTACHMENT_LINKEDMESH`. rigc emits region, mesh, boundingbox, clipping and path | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what each deferred type would carry |
542
+ | `ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so on the one type left it reads `ATTACHMENT_POINT`. rigc emits region, mesh, linkedmesh, boundingbox, clipping and path — `linkedmesh` since [#691](https://github.com/firejune/rigc/issues/691), and `point` is the remaining deferred type | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what a deferred type would carry |
543
543
  | `ATTACHMENT_NAME` | `LOSS` | 0 | the attachment states a `name` and only **one** skin fills the placeholder, so rigc writes none — it composes `<skin>/<placeholder>` exactly where a placeholder is contested | nothing, unless something downstream looks that attachment up by the name the source gave it |
544
544
  | `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | the attachment carries a `sequence` block — a numbered image series — which the rig spec cannot say | the rebuild draws the single region the attachment names; the frames have to be driven some other way |
545
545
  | `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline other than `deform`, which is the only one the motion spec carries | transcribe it, or accept that the rebuild does not play it |
@@ -559,7 +559,8 @@ is the one failure a comparison of two sets cannot show you.
559
559
  | `PHYSICS_TIMELINE` | `BLOCK` | 1 | the same for a physics constraint, whose eight the motion spec carries in full — so this is reachable only for a name the **parser** falls through too | as `PATH_TIMELINE` |
560
560
  | `SLIDER_TIMELINE` | `BLOCK` | 1 | the same for a slider, which carries time and mix | as `PATH_TIMELINE` |
561
561
  | `SLOT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a slot | as `BONE_FIELD` |
562
- | `SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline that is not `attachment` or `rgba`. The format also has `rgb`, `alpha`, `rgba2` and `rgb2`, and the motion spec has no track for any of them | transcribe it, or accept that the rebuild plays nothing there |
562
+ | `SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline the motion spec has no track for. The detail names the ones it does have and the ones the format has left over, **both read off the tables** rather than listed here: this cell named `rgba2` among the timelines nobody carries until [#690](https://github.com/firejune/rigc/issues/690) made that false, which is what a hand-kept list beside a derived one always comes to | transcribe it, or accept that the rebuild plays nothing there |
563
+ | `SPEC_REFUSED` | `BLOCK` | 1 | the specs were written and **rigc's own parser refuses one of them** — the detail carries that refusal word for word, after the file and the spec it is about. It is the one finding that is not about a single construct: it is whatever `parseRigSpec` or `parseMotionSpec` names, from a shape the format holds and the spec cannot say (a constraint that is `skinRequired` under no skin) to a defect in this decompiler. Until [#692](https://github.com/firejune/rigc/issues/692) the refusal left through `ingest` itself, so the run exited 1 with no line, no code and no `findings.json` at all | read the quoted sentence against the skeleton: it names the object. Both specs are on disk for exactly that, and `build` will refuse them until the shape has a spelling — [AUTHORING §5.1](AUTHORING.md) is the list of what a parser says |
563
564
  | `TIMELINE_FIELD` | `BLOCK` | 1 | a key field on a bone, path, physics or slider timeline that is not part of that timeline's shape | check the spelling; the field is dropped from the rebuilt key |
564
565
  | `TIMELINE_KEY_RESTATED` | `LOSS` | 0 | **the commonest line in a real run.** An editor omits a channel that equals the parser's default; the motion spec's `v` is positional, so the omission is written out at that default | nothing. The same values the runtime reads, spelled out — a larger file and the same animation |
565
566
  | `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
@@ -608,7 +608,7 @@ Legend: ✅ emits · 🟡 partial · ❌ not emitted · 🚫 deliberately exclud
608
608
  | `name`, `bone` | ✅ |
609
609
  | `attachment` | ✅ — only when the motion spec's `setup` block names one; `null` means "show nothing" and the key is omitted |
610
610
  | `color` | ✅ — from `setup.color`, hex-encoded (`compile.ts:63-66`) |
611
- | `dark` | 🚫 **A12_NO_DARK_COLOR** (`validate.ts:250-262`) |
611
+ | `dark` | ✅ — copied from the rig spec's slot when it gives one. 🚫 **A12_NO_DARK_COLOR** still names it, and that is a **renderer-profile** verdict rather than a validity one: `build` runs `--profile spine`, where A12 reports `PROF`. **A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN** reads it back off the runtime |
612
612
  | `blend` | ✅ — copied from the rig spec's slot when it gives one (`compile.ts:600`, landed in `c5eda3b`) |
613
613
  | `visible` | ❌ |
614
614
  | draw order = slots array order | ✅, and **A26_SLOT_DRAW_ORDER** pins it to the archetype's `slotOrder` table |
@@ -626,9 +626,9 @@ with the member's own `skin: true` — either half alone is refused, because `Sk
626
626
  | --- | --- | --- |
627
627
  | `region` | 🟡 | emits `width`, `height` (always, from PNG measurement — the fix for case 6c), `x`, `y` (only when non-zero), `rotation` (only when non-zero, cancelling the bone's world rotation), `scaleX`/`scaleY` (`compile.ts:1238-1239`), `color`, and `path` — taken from the rig spec, else derived from the image basename and omitted when that basename *is* the attachment name (the region name == attachment name == PNG basename convention **A08** + **A27** enforce). ❌ `sequence` |
628
628
  | `mesh` | 🟡 | emits `type`, `uvs`, `triangles`, `vertices` (**weighted encoding only**), `hull`, `width`, `height`, `edges` (`compile.ts:1343`, and part 4's rung-6 entry measures it byte-identical to the reference), `path`, `color`. ❌ `sequence`. Unweighted meshes are 🚫 **A20_MESH_WEIGHTS_COHERENT** (`validate.ts:430-433`) |
629
- | `linkedmesh` | ❌ | deliberately deferred — never appears in the corpus (part 3-1) |
629
+ | `linkedmesh` | 🟡 | emits `type`, `source`, its own `path`, `width`, `height` and `color`, and `slot`/`skin`/`timelines` **only where they differ from the parser's defaults** (this attachment's slot, the default skin, true). ❌ `sequence`. Geometry keys on a link are refused by name — the parser returns before `readVertices`, so it reads none of them. A chain (a `source` that is itself a link) and a link to itself are refused: they resolve in file order and load `worldVerticesLength` 0 in one of the two orders, silently. `A21`/`A28` leave a link out (its rim and rows are its source's), `A04`/`A20`/`A22` read it as the mesh it resolves to, `A13` counts it as a mesh slot of its own ([#691](https://github.com/firejune/rigc/issues/691)) |
630
630
  | `boundingbox` | ✅ | `vertexCount` (required and cross-checked), `vertices` **or** by-name `weights`, `color`. **A33_VERTEX_ATTACHMENT_GEOMETRY** |
631
- | `path` | ✅ | `vertexCount` (required, and checked as a multiple of 3 — the parser's own `vertexCount / 3` takes a fractional size in silence), `vertices` **or** by-name `weights`, `closed`, `constantSpeed`, `color`, and a **measured** `lengths`: the cumulative setup length at the end of each curve, taken through each influence's own bone and measured as `PathConstraint` measures it — its own four-sample forward difference, which is what the editor writes too and is about 0.5 % below the arc (AUTHORING §10.6) — refused if authored. **A33_VERTEX_ATTACHMENT_GEOMETRY** re-checks the structure and the array's monotonicity. ❌ a `deform` timeline on one |
631
+ | `path` | ✅ | `vertexCount` (required, and checked as a multiple of 3 — the parser's own `vertexCount / 3` takes a fractional size in silence), `vertices` **or** by-name `weights`, `closed`, `constantSpeed`, `color`, and a **measured** `lengths`: the cumulative setup length at the end of each curve, taken through each influence's own bone and measured as `PathConstraint` measures it — its own four-sample forward difference, which is what the editor writes too and is about 0.5 % below the arc (AUTHORING §10.6) — refused if authored. **A33_VERTEX_ATTACHMENT_GEOMETRY** re-checks the structure and the array's monotonicity. A `deform` timeline may key one: the array is the control points, `A39` SKIPs on it (no triangles), and `lengths` stays the setup measurement because the format has nowhere to put a per-key one — which only a `constantSpeed: false` traversal reads (AUTHORING §4.11) |
632
632
  | `point` | ❌ | deliberately deferred — never appears in the corpus (part 3-1) |
633
633
  | `clipping` | ✅ under `--profile spine` · 🚫 under `spine-html` | `end` (refused when it names no slot), `convex`, `inverse`, `vertexCount`, geometry, `color`. **A33**, and **A11_NO_CLIPPING_ATTACHMENTS** is the renderer-profile refusal |
634
634
  | `sequence` block | ❌ | |
@@ -676,7 +676,8 @@ editor-made meshes — those arrive as authored `uvs`/`triangles`/`weights`.
676
676
  | `slots.attachment` | ✅ — value `name`, nullable; easing is refused |
677
677
  | `slots.rgba` | ✅ |
678
678
  | `slots.rgb`, `alpha` | ❌ emitted (validator knows the channel counts) |
679
- | `slots.rgba2`, `rgb2` | 🚫 **A12_NO_DARK_COLOR** (`validate.ts:257-261`) |
679
+ | `slots.rgba2` | ✅ — `{time, light, dark}`, seven channels as `[lr, lg, lb, la, dr, dg, db]` in the motion spec ([#690](https://github.com/firejune/rigc/issues/690)). 🚫 **A12_NO_DARK_COLOR** names it under `spine-html` only; **A43** poses it and compares |
680
+ | `slots.rgb2` | ❌ emitted (validator knows the channel count). 🚫 **A12_NO_DARK_COLOR** under `spine-html` |
680
681
  | `physics.inertia/strength/damping/mass/wind/gravity`, `physics.mix`, `physics.reset` | ✅ — all eight (`PHYSICS_TRACKS` in `compile.ts`), authored as `tracks` entries naming `physics`. ⚠️ Their per-key defaults are the parser's, and part 1-8 above is the source: 0 on the six, 1 on `mix`. Not the constraint defaults at `:306-312` |
681
682
  | `ik`, `transform` | ✅ — one unnamed timeline per constraint, keyed by the motion spec's `ik` / `transform` arrays |
682
683
  | `path.position/spacing/mix`, `slider.time/mix` | ✅ (`PATH_TRACKS` / `SLIDER_TRACKS`) — authored as `tracks` entries naming `path` or `slider`, the same shape as `physics`, since both groups put named timelines under a constraint name. `path.mix` is one timeline of three channels |
@@ -731,9 +732,10 @@ This is the split Part 4(c) needs. **Spine-validity** = the file is wrong for an
731
732
  | `A16_SKELETON_VERSION_4_3` | validity (portability) | a `spine` string outside `4.3.x` |
732
733
  | `A17_ATLAS_PAGE_FILES_EXIST` | validity | a page PNG not on disk |
733
734
  | `A18_DETERMINISTIC_EMIT` | tool contract | recompiling differs byte-for-byte |
734
- | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | **mixed** | size≠PNG is **validity** (case 6h); `pma:true`, region rotation, and "region covers the whole page" are **renderer-profile** |
735
+ | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | **mixed** | size≠PNG is **validity** (case 6h), and so is a region whose rectangle is outside the page it names, rotation honoured ([#694](https://github.com/firejune/rigc/issues/694)); `pma:true`, region rotation, and two regions on one page over the same texels are **renderer-profile** |
735
736
  | `A11_NO_CLIPPING_ATTACHMENTS` | **renderer-profile** | clipping attachments — "the renderer skips them silently" |
736
- | `A12_NO_DARK_COLOR` | **renderer-profile** | slot `dark`, `rgba2`/`rgb2` timelines — "parsed, then ignored" |
737
+ | `A12_NO_DARK_COLOR` | **renderer-profile** | slot `dark`, `rgba2`/`rgb2` timelines — "parsed, then ignored". ⚠️ rigc **emits** the first two; a renderer that drops a construct is what a profile is for, not a reason not to emit it |
738
+ | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | validity | a slot `dark` the parser drops or reads as NaN, an `rgba2` timeline on a slot with no dark colour to pose, or a key whose posed light/dark is not what it states |
737
739
  | `A13_MESH_BUDGET` | **renderer-profile** | >4 mesh slots, >80 triangles per mesh |
738
740
  | `A14_NO_FULL_FRAME_MESH` | **renderer-profile** | a mesh spanning the whole stage |
739
741
  | `A19_OVERLAY_PNGS_HAVE_ALPHA` | **renderer-profile** | an overlay page that can never be transparent — no alpha channel and no `tRNS` chunk |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.25.6",
3
+ "version": "0.26.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": {