spine-rigc 0.22.2 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/INGEST.md CHANGED
@@ -50,12 +50,22 @@ invent one.
50
50
  Two facts decide everything below, and they pull in opposite directions:
51
51
 
52
52
  1. **rigc reads compiled skeleton JSON in more places than you would guess.**
53
- `validate`, `render`, `preview`, `vote`, `check` and `diff` all take a
54
- `skeleton.json` path directly, and none of them needs a rig spec to do it.
55
- 2. **rigc cannot write one back.** There is no command that edits a skeleton, no
56
- importer, and no route from skeleton JSON to specs. The only thing that produces a
57
- skeleton is `build`, and `build`'s input is a rig spec plus a motion spec.
58
- ⇒ **Every route that ends in a changed file goes through transcription** (§2).
53
+ `validate`, `render`, `preview`, `vote`, `check`, `diff` and — since #569 —
54
+ `ingest` all take a `skeleton.json` path directly, and none of them needs a rig
55
+ spec to do it.
56
+ 2. **rigc still cannot EDIT one.** There is no command that opens a skeleton and
57
+ changes it. The only thing that produces a skeleton is `build`, and `build`'s
58
+ input is a rig spec plus a motion spec.
59
+ ⇒ **Every route that ends in a changed file goes through the specs** — and
60
+ since #569 there are two ways to get them: write them (§2, transcription) or
61
+ have `ingest` write them for you from the file itself (§2.0).
62
+
63
+ ⚠️ This clause read *"rigc cannot write one back… no route from skeleton JSON
64
+ to specs"* until 2026-09-17, and the half that was wrong is the second half.
65
+ The route exists now and its contract is an equality — `build(ingest(x))` is
66
+ `x`, byte for byte — which is a stronger statement than anything transcription
67
+ could make. What survives is the first half: nothing **edits** a skeleton, and
68
+ the specs remain the only thing a change is expressed in.
59
69
 
60
70
  ### 0.1 The table
61
71
 
@@ -71,6 +81,7 @@ an upstream `license.txt` (Appendix, and [NOTICE.md](../NOTICE.md)).
71
81
  | **`vote --candidate <a> --candidate <b>`** | ✅ **yes, on either side** | a ballot page. Pairing a foreign export against your own transcription is a legitimate ballot, and the panes carry no paths |
72
82
  | **`check --candidate <skeleton.json> --frames <dir>`** | ✅ **yes** | ⭐ it reads **frames and never a reference skeleton**, so a foreign export enters this one *twice over*: as the candidate, or — via `render` — as the source of the frames. §1.4 |
73
83
  | **`diff <candidate.json> <reference.json>`** | ✅ **yes, both sides** | 49 structural measures over bones, slots, attachments, constraints, animations and events. ⛔ **Blind to every coordinate** — §1.3 |
84
+ | **`ingest <skeleton.json> --out <dir>`** | ✅ **yes — and it is the only reader that WRITES specs** | the `.json` alone; no atlas, no art, no project file. Out come `rig.json`, `motion.json` and a findings report, such that `build`ing them reproduces the skeleton it read **byte for byte**. The seventh reader, and the one that ends §2's hand work — §2.0 and §5 |
74
85
  | **`pose --images <dir> --frame <png>`** | ⛔ **not the skeleton** | loose part PNGs and one picture. A packed atlas page is not loose parts, and pointing it at one produces a confident answer about nothing — §5 |
75
86
  | **`explain --rig … --motion … --out …`** | ⛔ **no** | rig spec + motion spec. It explains **what you wrote**, which makes it a transcription instrument rather than a reading one — §1.5 |
76
87
  | **`build --rig … --motion … --images …`** | ⛔ **no** | specs in, skeleton out. The only writer in the toolchain, and the reason §2 exists |
@@ -151,7 +162,11 @@ rigc validate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.
151
162
  SKIP A32_EVENT_KEYS_RESOLVE: no animation carries an event timeline
152
163
  SKIP A34_CONSTRAINT_TIMELINE_TARGETS: no animation carries a constraint timeline
153
164
  SKIP A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: no animation carries a deform timeline
165
+ SKIP A04_MESH_TRIANGLES_AND_ENCODING: the skeleton carries no mesh attachment
154
166
  SKIP A33_VERTEX_ATTACHMENT_GEOMETRY: the skeleton carries no bounding box, clipping attachment or path
167
+ SKIP A20_MESH_WEIGHTS_COHERENT: the skeleton carries no mesh attachment
168
+ SKIP A22_MESH_UVS_IN_UNIT_RANGE: the skeleton carries no mesh attachment
169
+ SKIP A23_PHYSICS_CONSTRAINT_EFFECTIVE: the skeleton declares no physics constraint
155
170
  …
156
171
  PROF A12_NO_DARK_COLOR: renderer rule, not in profile "spine"
157
172
  PROF A21_MESH_RIM_PINNED: archetype rule, not in profile "spine"
@@ -164,10 +179,17 @@ Read it as three separate statements, because they answer three different questi
164
179
  - **`PASS` / `FAIL`** — the rule ran, and this is its verdict.
165
180
  - **`SKIP`** — the rule ran and had nothing to measure, and the line says what was
166
181
  absent. A `SKIP` is never a pass, and on foreign data the `SKIP` list is also a
167
- **free inventory of what the skeleton does not contain**. The five lines above tell
182
+ **free inventory of what the skeleton does not contain**. The nine lines above tell
168
183
  you, without your having opened the JSON, that this export has no draw-order
169
- timeline, no event timeline, no constraint timeline, no deform timeline, and no
170
- bounding box, clipping attachment or path.
184
+ timeline, no event timeline, no constraint timeline, no deform timeline, no mesh
185
+ attachment, no physics constraint, and no bounding box, clipping attachment or
186
+ path. ⭐ Four of those lines used to read `PASS`
187
+ ([#580](https://github.com/firejune/rigc/issues/580)): a rule that walks the
188
+ meshes, or the physics constraints, and finds none has measured nothing, and
189
+ reporting that as held both overstated the gate and cost you the inventory line.
190
+ What still passes over an empty list is the other kind of rule — `A01`, `A02`,
191
+ `A11`, `A12`, `A14` ask *how many of this does the file carry*, and **zero is the
192
+ answer**.
171
193
  - **`PROF`** — the rule was excluded by the profile before its body ran. §3.3.
172
194
 
173
195
  ⚠️ **A green here is a statement about validity and nothing else.** It does not say
@@ -212,9 +234,11 @@ Spine runtime plays it, whatever rigc's own rasteriser or validator thinks.
212
234
 
213
235
  ### 1.3 `diff` — and the two things it cannot see
214
236
 
215
- `diff` takes two compiled skeletons and reports 49 measures in eight groups. Both
216
- sides may be foreign; the interesting pairing during ingest is **your transcription
217
- against the export it came from**:
237
+ `diff` takes two compiled skeletons and reports 49 measures in eight groups, plus two
238
+ blocks that report and gate nothing: the `(reported)` measures beside `attachments`
239
+ and `animations`, and the `skeleton` header block at the top, which measures the stage
240
+ (issue #578). Both sides may be foreign; the interesting pairing during ingest is
241
+ **your transcription against the export it came from**:
218
242
 
219
243
  ```bash
220
244
  rigc diff work/t3/skeleton.json \
@@ -227,6 +251,10 @@ rigc diff
227
251
  reference …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
228
252
  .. bones=3/3 slots=2/2 skins=1/1 attachments=2/2 constraints=0/0 animations=2/2 events=0/0 (candidate/reference)
229
253
 
254
+ skeleton (reported) (no mean) over 2 measures — the stage, which no reading of the frames could decide
255
+ 1.000 stage_present 1/1 both sides declare a setup-pose stage, or neither does — …
256
+ 1.000 stage_box 4/4 the stage is the same box (x, y, width, height, exactly as stated) — …
257
+
230
258
  bones mean 1.000 over 8 measures
231
259
  1.000 count 3/3 how many bones
232
260
  1.000 names 3/3 the bone names themselves
@@ -253,11 +281,18 @@ Each pair is **matched / total**, where the total is the larger of the two sides
253
281
  count of `2/3` means one side has three of something and only two were matched — and
254
282
  it does not say *which* side has three. The `..` line above is where you read that.
255
283
 
256
- ⛔ **`diff` is blind to every coordinate.** No measure reads a bone's
257
- `x`/`y`/`rotation`, an attachment's offset, or a key's value — only *presence*,
258
- *names*, *counts*, *order* and *kinds*. §4.1 moves a pivot 236.5 units and every one
259
- of the 49 measures still reads **1.000**. ⇒ Never take a green `diff` as evidence that
260
- a geometric edit did not land, and never take it as evidence that one did.
284
+ ⛔ **`diff` is blind to every coordinate a bone, an attachment or a key carries.** No
285
+ measure reads a bone's `x`/`y`/`rotation`, an attachment's offset, or a key's value —
286
+ only *presence*, *names*, *counts*, *order* and *kinds*. §4.1 moves a pivot 236.5
287
+ units and every one of the 49 measures still reads **1.000**. ⇒ Never take a green
288
+ `diff` as evidence that a geometric edit did not land, and never take it as evidence
289
+ that one did.
290
+
291
+ ⚠️ **The one exception is the skeleton's own declared box**, and it is an exception to
292
+ the sentence and not to the rule: `skeleton.stage_box` compares four world numbers,
293
+ but they are numbers an exporter *wrote into the header* rather than a pose anything
294
+ measured, and the block they sit in gates nothing. Moving a pivot does not move them
295
+ either.
261
296
 
262
297
  ⛔ **And its ratios are not a score.** [`src/diff.ts`](../src/diff.ts) says so in the
263
298
  type itself (*"Unweighted mean of the measures below. NOT a quality score"*), and the
@@ -377,17 +412,90 @@ is fine — it is not created.
377
412
 
378
413
  ---
379
414
 
380
- ## 2. Transcription — the route that makes a foreign skeleton yours
415
+ ## 2. Getting specs out of a skeleton
381
416
 
382
- Everything in §1 reads. To **change** anything you need specs, and getting specs out
383
- of a skeleton is a job rigc does not do for you: the numbers come out of the JSON by
384
- hand, into a rig spec and a motion spec, and `build` emits a new skeleton from those.
417
+ Everything in §1 reads. To **change** anything you need specs. There are two routes
418
+ to them and you should almost always take the first.
385
419
 
386
- ⚠️ **Say this to the user before starting, because it is the part that surprises.**
387
- Transcription is not a conversion step you run; it *is* the work. What you get for it
388
- is that the file becomes editable by declaration — after transcription a pivot move is
389
- two numbers in a spec (§4.1) and a new animation is an added block (§4.3), where
390
- before it was a hand-edit of emitted JSON with nothing checking it.
420
+ ### 2.0 `ingest` — let the tool write them
421
+
422
+ ```bash
423
+ rigc ingest examples/spineboy/export/spineboy-ess.json --out specs/ --art none
424
+ rigc build --rig specs/rig.json --motion specs/motion.json --atlas-in examples/spineboy/export/spineboy.atlas --out spine
425
+ rigc diff spine/skeleton.json examples/spineboy/export/spineboy-ess.json
426
+ ```
427
+
428
+ `ingest` reads the skeleton — **only** the skeleton — and writes `rig.json`,
429
+ `motion.json` and `findings.json`. The contract is an equality rather than a
430
+ rulebook: `build(ingest(x))` is `x`, byte for byte on `skeleton.json`, and the
431
+ atlas comes back with the same region blocks (as a multiset — the page order is in
432
+ no field of the file). `bun run selftest` holds every rig this repository builds to
433
+ that on every run, which is the one gate here that compares an emitted file against
434
+ a file rigc did not write.
435
+
436
+ 📊 **And it holds the twelve editor exports to the weaker claim that is available for
437
+ them** ([#594](https://github.com/firejune/rigc/issues/594)). Every
438
+ `examples/*/export/*.json` is ingested with `--art none`, rebuilt through the pack
439
+ beside it, and `diff`ed against the file it was read from: **12 of 12 come back with 0
440
+ blockers and 1.000 on all 49 ratio-bearing measures and all 5 reported ones.** Byte
441
+ identity is not the claim there and the reason is the input, not the round trip — §2.3
442
+ has the three kinds of difference, measured. ⚠️ Which pack is "the one beside it" is
443
+ resolved rather than guessed, for §0.2's reason: `spineboy/export` holds two, and
444
+ `spineboy-run.atlas` covers neither skeleton in it.
445
+
446
+ **What it will not do is invent.** Everything the spec format cannot hold is a
447
+ finding with a code — `BLOCK` for a construct the rebuild will be missing, `JUDGE`
448
+ for the two values a skeleton does not carry, `LOSS` for the one number rigc
449
+ re-derives on purpose. A blocker exits non-zero and still writes both files.
450
+
451
+ **Two values are not in a skeleton**, so `ingest` asks rather than guesses:
452
+
453
+ - **the stage** (`skeleton.width`/`height`) — `--stage x,y,w,h` is how you supply one
454
+ when the file has none. ⚠️ **This page said an editor export carries none until
455
+ [#594](https://github.com/firejune/rigc/issues/594) measured it: all twelve exports in
456
+ the fetched corpus carry a stage**, `ingest` reads it straight through, and not one of
457
+ them needed the flag. What holds without qualification is that the box cannot be
458
+ *derived* — posing the rig gives the *animated* extent, which is a different number
459
+ from the setup box — so a skeleton that really declares none is a `NO_STAGE` blocker
460
+ rather than a guess. It is also the value that costs least to get wrong: `diff`
461
+ reports it as two measures of its own (`stage_present`, `stage_box`, since
462
+ [#578](https://github.com/firejune/rigc/issues/578)) and they are `(reported)`, so
463
+ nothing on the ladder reads them and an absurd box is green nearly everywhere. The
464
+ corpus half of the selftest's `IG` suite is the one gate that does read them;
465
+ - **each animation's duration** — the format has no such field. The largest key time
466
+ is used, stated in the motion spec's `note`, and recorded as a finding per
467
+ animation. Edit it if you know the real number.
468
+
469
+ And two flags for what the skeleton also does not encode: `--art loose` (the default)
470
+ names an `image` per attachment resolved against loose PNGs, `--art none` states
471
+ `width`/`height` for `build --atlas-in`; and under `loose`, `--images <dir>` writes
472
+ the rig spec's own images directory relative to `--out`, so the rebuild is a plain
473
+ `build --rig … --motion … --out …` rather than one carrying `--images` forever. It is
474
+ refused together with `--art none`, which writes no `image` for a directory to be the
475
+ base of. [AUTHORING §0.3](AUTHORING.md) is the loop in full.
476
+
477
+ 📝 Both written specs carry a `note` saying they are decompiled and naming the file
478
+ they came from. Leave it there — §2.4 is why.
479
+
480
+ ### Transcription — the route that made a foreign skeleton yours
481
+
482
+ ⚠️ **The rest of §2 is the route that existed before #569, and it is kept because
483
+ the reading it produces is still the right one** — it is what an author does *after*
484
+ `ingest`, and it is what to fall back on for the constructs `ingest` reports as
485
+ blockers. The numbers come out of the JSON into a rig spec and a motion spec by hand,
486
+ and `build` emits a new skeleton from those.
487
+
488
+ What you get for it is that the file becomes editable by declaration — a pivot move
489
+ is two numbers in a spec (§4.1) and a new animation is an added block (§4.3), where
490
+ before it was a hand-edit of emitted JSON with nothing checking it. That is now what
491
+ `ingest` hands you in one command; the sections below are how to read and change what
492
+ it hands you, and every rule in them applies to a spec `ingest` wrote.
493
+
494
+ 📌 **The cost this section used to warn about is measured, and it is why §5 changed.**
495
+ The smallest skeleton of the corpus behind [#569](https://github.com/firejune/rigc/issues/569)
496
+ transcribed to a **257,422-byte** rig spec, of which 91.8 % is the six geometry
497
+ arrays — numbers, not decisions. A 558-line prototype decompiler reproduced 100 % of
498
+ it, and the only differing paths were the name and the `note`.
391
499
 
392
500
  ### 2.1 The workflow
393
501
 
@@ -409,6 +517,32 @@ before it was a hand-edit of emitted JSON with nothing checking it.
409
517
  copying and not a detail. Leave `invariants` out entirely — it describes rigc's own
410
518
  formations, and an absent field makes an archetype assertion `SKIP`, never pass
411
519
  (AUTHORING §3.7).
520
+
521
+ 📌 **Transcribe the export's empty slots too** — the ones no skin fills anywhere.
522
+ Such a slot still holds an index in the array, and everything below it is counted
523
+ from that index. Write it as `{ "name": …, "bone": … }` with no `attachment`, or
524
+ with `"attachment": null` if you prefer to say it out loud; either way it comes
525
+ back. Before issue #575 it did not: `build` dropped it in silence, so two exports
526
+ declaring 53 and 61 slots came back at 51 and 57 with a green gate, and `diff`
527
+ read 0.962 and 0.934 against the file they had been read from. If a
528
+ transcription's `slots.count` is under 1.000, this is the first thing to check.
529
+
530
+ ⚠️ **No skeleton in `examples/` has one**, which is why the corpus never showed
531
+ this: all twelve exports fill every slot they declare from some skin. What they
532
+ *do* carry is the neighbouring shape — a slot a skin DOES fill whose setup pose
533
+ shows nothing (34 of `spineboy-pro`'s 52 slots). Both are written the same way in
534
+ the file: `attachment` simply absent.
535
+
536
+ ⚠️ **If the export's `skeleton` block carries no `x`/`y`/`width`/`height`, write
537
+ `"width": null, "height": null` and do not invent one** (issue #578). That shape is
538
+ common — the twelve exports in `examples/` all carry the four, and 37 of 37 exports
539
+ in one production corpus carry none of them — and until the `null` pair existed the
540
+ only two moves were a made-up stage or a file that could not be transcribed. The
541
+ made-up stage was the worse one: it is a number nothing in this toolchain could
542
+ contradict, so it survived every gate and every `diff` in silence. Now it does not —
543
+ `diff`'s header block reports `skeleton.stage_present` and `skeleton.stage_box`
544
+ against the source you are copying. Copy the four numbers when they are there;
545
+ state the absence when they are not.
412
546
  4. **`explain`, then `build`.** `explain` first, because it prints what you wrote in a
413
547
  shape you can compare against the export by eye (§1.5) and it never gates. Then
414
548
  `build` under `--profile spine`.
@@ -455,6 +589,25 @@ State the ambition in the right units, because three different things get called
455
589
  | **Geometric agreement** — the same drawn pixels, allowing for the atlas | ✅ yes, and `check` measures it | see below |
456
590
  | **Byte-identical JSON** | ⛔ **no, and not because of the geometry** | rigc writes defaults explicitly where the editor omits them, and the editor writes bookkeeping rigc has no field for. SPEC_COVERAGE records the count on rung 6: a field-by-field comparison against the reference export leaves **49 differences, every one benign** — 39 explicit defaults, 3 editor bookkeeping keys, 1 runtime version string, and 6 bone `icon` values, which was the only thing the rig spec could not say at all |
457
591
 
592
+ ⚠️ **The third row holds for `ingest` too, and it is worth knowing in which direction.**
593
+ `build(ingest(x))` is byte-identical for a skeleton **rigc** emitted — that is the
594
+ contract `bun run selftest` gates on every run — and it is not, for a skeleton the
595
+ editor emitted. Measured over all twelve corpus exports, a field-by-field walk of the
596
+ rebuild against its source produces differences of exactly three kinds, in every file:
597
+
598
+ | Kind | Example, candidate vs reference | Why |
599
+ | --- | --- | --- |
600
+ | **header bookkeeping**, 3 per file | `skeleton.hash: undefined vs "VFWbaK2UoCM"`, `skeleton.audio: undefined vs null`, `skeleton.spine: "4.3.13" vs "4.3.75-beta"` | the rig spec has no field for `hash` or `audio`, and the version is the runtime rigc links. `ingest` reports all three as findings — `HEADER_BOOKKEEPING` and `HEADER_REDERIVED` |
601
+ | **an omitted default written out** | `…rotate[0].time: 0 vs undefined` | the editor omits a zero `time`; rigc writes it. AUTHORING §10.5's *do not imitate the exporter's omissions*, from the other side |
602
+ | **the emitted precision** | `…curve[0]: 0.066667 vs 0.06666667`, `uvs[0]: 0 vs 2.554152e-7` | rigc emits six decimals |
603
+
604
+ ⇒ **So the corpus gate is `diff` at 1.000 rather than a byte comparison**, and it is
605
+ worth being exact about what that does and does not cover. `diff` compares structure —
606
+ counts, names, parentage, order, timeline kinds, key counts, curve kinds — and **not
607
+ the values inside the keys**, which is why the precision row above is invisible to it.
608
+ On rigc's own rigs byte identity covers both; on a foreign export the values are held
609
+ by `check` (pixels) or by nothing, depending on what you render.
610
+
458
611
  The geometric row needs a real number, because a naive reading of `check` makes an
459
612
  exact transcription look wrong. Here is the 3-timing transcription against frames
460
613
  rendered from the export it was transcribed from:
@@ -656,12 +809,22 @@ other one is this project's own renderer and archetype policy.
656
809
  | **renderer policy** (7) | `A11_NO_CLIPPING_ATTACHMENTS`, `A12_NO_DARK_COLOR`, `A13_MESH_BUDGET`, `A14_NO_FULL_FRAME_MESH`, `A15_IDLE_NO_MESH_BONE_KEYS`, `A19_OVERLAY_PNGS_HAVE_ALPHA`, `A27_REGION_NAME_MATCHES_PAGE_FILENAME` |
657
810
  | **archetype policy** (8) | `A21_MESH_RIM_PINNED`, `A24_AXIS_SPACE_STROKE`, `A25_DETACHED_BONE_PARENTAGE`, `A26_SLOT_DRAW_ORDER`, `A28_RIBBON_ROWS_SHARE_WEIGHTS`, `A29_STROKE_WITHIN_CONTACT_DEPTH`, `A30_STROKE_WITHIN_CAP_CONTAINMENT`, `A39_DEFORM_KEEPS_TRIANGLE_WINDING` |
658
811
 
659
- Three further rules — **`A06`**, **`A08`** and **`A20`** — are *mixed*: their validity
660
- clauses run in both profiles and their policy clauses only under `spine-html`. `A06`'s
812
+ Two further rules — **`A06`** and **`A20`** — are *mixed*: their validity clauses run
813
+ in both profiles and their policy clauses only under `spine-html`. `A06`'s
661
814
  size-vs-PNG check is validity; one-part-per-page coverage, rotation and premultiplied
662
- alpha are policy. `A08`'s attachment→region join is validity; requiring the two names
663
- to be *identical* is policy. `A20`'s weight coherence is validity; requiring a mesh to
664
- be weighted at all is policy.
815
+ alpha are policy. `A20`'s weight coherence is validity; requiring a mesh to be
816
+ weighted at all is policy.
817
+
818
+ **`A08` was the third until [#574](https://github.com/firejune/rigc/issues/574).** Its
819
+ policy clause required a skin entry's placeholder to be spelled exactly like the region
820
+ it resolves to — a rule the renderer it was gated under never performed, since
821
+ `spine-html` keys its images on the atlas region name reached through the attachment's
822
+ `path` and reads no placeholder at all. Measured before retiring it: the clause fired
823
+ on **0** attachments across the whole example corpus (no export in `examples/` carries
824
+ a `path` field), and on every rigc rig whose placeholder is not its PNG's basename —
825
+ which is what `path` exists for (AUTHORING §2, R5) and what a placeholder two named
826
+ skins share is emitted as. So it was policy that only ever refused this compiler's own
827
+ correct output.
665
828
 
666
829
  ⚠️ **`--profile spine-html` on foreign data produces a wall of failures that mean
667
830
  nothing about the file.** Same `spineboy-pro.json`, same atlas, one flag changed — the
@@ -918,10 +1081,14 @@ Three things to read out of that, in order:
918
1081
  something is invisible to every measure in that report. Pair it with a `check` against
919
1082
  frames rendered from the original.
920
1083
 
921
- ⚠️ **Do not rename toward what a rule seems to want.** `A08`'s name-identity clause and
922
- `A27`'s region-name-matches-page-filename are both `spine-html` policy (§3.3): under
923
- the default profile they do not fire, and renaming somebody's attachments to satisfy a
924
- policy they never opted into is a change with no benefit to them.
1084
+ ⚠️ **Do not rename toward what a rule seems to want.** `A27`'s
1085
+ region-name-matches-page-filename is `spine-html` policy (§3.3): under the default
1086
+ profile it does not fire, and renaming somebody's attachments to satisfy a policy they
1087
+ never opted into is a change with no benefit to them. `A08` carried a name-identity
1088
+ clause of the same kind until
1089
+ [#574](https://github.com/firejune/rigc/issues/574) retired it, and that one is the
1090
+ argument's own case study — the rename it seemed to want was one no renderer had ever
1091
+ asked for.
925
1092
 
926
1093
  ### 4.3 Extending a foreign skeleton with a new animation
927
1094
 
@@ -1010,12 +1177,36 @@ dependency *can* read it and rigc *does not*:
1010
1177
  that already has the reader, not a parser to write. But it is not there, and nothing on
1011
1178
  this page works on a `.skel` today. Re-export as JSON.
1012
1179
 
1013
- 🚫 **No skeleton-to-spec decompiler.** Nothing turns skeleton JSON back into a rig spec
1014
- and a motion spec. §2 is hand work, and that is the current state rather than a
1015
- temporary one: a decompiler would have to invent the things the spec format exists to
1016
- make explicit — which pivot, which generator, which invariant — and the compiler's own
1017
- rule is that it never invents a value that is not in the spec. ⚠️ Not to be confused
1018
- with the *atlas* importer below, which is a different direction and does exist.
1180
+ ✅ **A skeleton-to-spec decompiler exists: `rigc ingest` (§2.0). This entry used to
1181
+ refuse one, and all three of its reasons were measured and refuted** — issue
1182
+ [#569](https://github.com/firejune/rigc/issues/569), 2026-09-17. The paragraph is
1183
+ kept below rather than deleted, because what it got wrong is more useful than a
1184
+ clean page:
1185
+
1186
+ > 🚫 ~~**No skeleton-to-spec decompiler.** Nothing turns skeleton JSON back into a rig
1187
+ > spec and a motion spec. §2 is hand work, and that is the current state rather than a
1188
+ > temporary one: a decompiler would have to invent the things the spec format exists to
1189
+ > make explicit — **which pivot, which generator, which invariant** — and the compiler's
1190
+ > own rule is that it never invents a value that is not in the spec.~~
1191
+
1192
+ | clause | what the measurement said |
1193
+ | --- | --- |
1194
+ | *which pivot* | ⛔ **refuted.** A bone's setup transform is in the skeleton, in full. 3,951 bones across 37 production exports and 15 rigs built from this tree were transcribed with **zero** decisions, and `diff`'s six bone measures — count, names, `parent_by_name`, order, `length_present`, `inherit_present` — read **1.000** on every file that built |
1195
+ | *which generator* | ⛔ **refuted, and the premise is the error.** A decompiler must choose **no** generator. A generator is a *model* (`src/rig.ts`: *"they encode a deformation model … and a model is not a table of numbers"*); the skeleton holds geometry, and geometry is what the rig spec's authored form takes. Inferring a model would be the invention this clause feared; writing the numbers is its opposite. `gallery/look`'s four generator-built meshes came back as authored geometry and the rebuild is **byte-identical** — so a generator can always be flattened, and that is the direction the information flows |
1196
+ | *which invariant* | ⛔ **refuted by omission, and this page already said how.** §2.1 step 3: *"Leave `invariants` out entirely — an absent field makes an archetype assertion SKIP, never pass."* `ingest` writes none. A decompiled spec is 91.8 % geometry, 8.2 % structure and **0 % intent**, and it says so instead of certifying something nobody measured |
1197
+
1198
+ ⇒ **What survives is the stage, and one value is not "the things the spec format
1199
+ exists to make explicit".** The clause was not wrong that a decompiler meets an
1200
+ invention — it was wrong about *which*, and wrong that it is unavoidable: a refusal
1201
+ naming the field is what this repository does with a missing number everywhere else,
1202
+ and it is what `ingest` does here (§2.0). ⚠️ Not to be confused with the *atlas*
1203
+ importer below, which is a different direction and also exists.
1204
+
1205
+ ⚠️ **What `ingest` is still not.** It reads skeleton JSON and writes two spec files.
1206
+ It does not read a `.spine` project or a binary `.skel` (the two entries above stand
1207
+ unchanged), it does not read the atlas or the art, it does not **edit** a skeleton,
1208
+ and it makes no claim about whether an agent could have *produced* the numbers it
1209
+ copied — only that the spec can carry them and `build` reproduces the file from them.
1019
1210
 
1020
1211
  ✅ **A packer and an importer both exist now, so do not report them as gaps.** This
1021
1212
  non-goal used to read *"rigc emits one region per page and cannot do otherwise"*, and
@@ -1090,9 +1281,12 @@ something is drawn over them are readable through its own draw order and hierarc
1090
1281
 
1091
1282
  📎 To be exact about what is missing: rigc *can* lift a region's drawing back off a
1092
1283
  page — `extractRegion` does it, and the contour mesh generator uses it under
1093
- `--atlas-in` — so what is absent is a **command**, not the capability. It refuses by
1094
- name on a region packed `rotate: 90` (AUTHORING §0.2), which a foreign pack can be and
1095
- rigc's own never is.
1284
+ `--atlas-in` — so what is absent is a **command**, not the capability. Since issue
1285
+ #570 that includes a region the pack **turned** (`rotate: 90`, `180`, `270`, or the
1286
+ older `rotate: true`), which a foreign pack routinely is and rigc's own never is: the
1287
+ lift transcribes `MeshAttachment.computeUVs`, the one routine in spine-core that
1288
+ states where a turned region's texels are, so what a generator measures does not
1289
+ depend on how the art was delivered (AUTHORING §0.2).
1096
1290
 
1097
1291
  🚫 **No `validate --fix`, and no normalisation pass.** Every recipe in §4 is a change
1098
1292
  you state in a spec and rebuild. A tool that rewrote somebody's export in place would
@@ -12,7 +12,7 @@ Reproduce the measurements with `bun run fetch-examples && bun run bench:usage`
12
12
 
13
13
  > 🔼 **This note is dated and does not move; the measurements below are the corpus as it
14
14
  > stood on 2026-08-22 and stay as written. Its statements about what rigc *does* have gone
15
- > stale in six places, and the live ledger is [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md):**
15
+ > stale in the places listed here, and the live ledger is [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md):**
16
16
  >
17
17
  > - **B2 is fixed** — A16 accepts `4.3`, `4.3.N` and `4.3.N-<suffix>`, so all twelve example
18
18
  > exports pass it.
@@ -48,6 +48,18 @@ Reproduce the measurements with `bun run fetch-examples && bun run bench:usage`
48
48
  > `length`/`scale`/`shear`/`inherit`/`skin`/`color` setup fields, slot `dark`/`blend`, region
49
49
  > `path`/`scaleX`/`scaleY`/`color`, mesh `path`/`edges`/`color`, and the header's `fps` /
50
50
  > `referenceScale` / `images`. Part 2's coverage tables predate all of it.
51
+ > - **A08's name-identity clause is retired (issue #574, 2026-09-17)** — so every line below
52
+ > that calls A08 *mixed* or names it as policy describes a rule that no longer exists:
53
+ > §2.1's `region` row ("the region name == attachment name == PNG basename convention
54
+ > **A08** + **A27** enforce" — A27 still enforces its half, A08 enforces none of it),
55
+ > §2.2's classification row and its "Nine assertions are renderer-profile or
56
+ > profile-mixed" paragraph, §4.1's row (c), and §4.3's item 2. A08 is plain **validity**
57
+ > now and identical under both profiles. The measurement behind it: `spine-html` resolves
58
+ > art through the attachment's `path` and keys its images on the atlas region name, in
59
+ > every published version of it, so the clause was a rule no renderer performed — and it
60
+ > fired on **0** attachments in this corpus, because no export in `examples/` carries a
61
+ > `path` field at all. The executive summary's item 8 is the one A08 line here that was
62
+ > already right and has only got more so: A08 is a Spine-validity rule.
51
63
  >
52
64
  > **The measurements of the CORPUS (Part 3) have not moved and stay as written.**
53
65
  >
@@ -646,8 +658,7 @@ editor-made meshes — those arrive as authored `uvs`/`triangles`/`weights`.
646
658
  | `slots.rgba` | ✅ |
647
659
  | `slots.rgb`, `alpha` | ❌ emitted (validator knows the channel counts) |
648
660
  | `slots.rgba2`, `rgb2` | 🚫 **A12_NO_DARK_COLOR** (`validate.ts:257-261`) |
649
- | `physics.mix`, `physics.reset` | ✅ (`PHYSICS_TRACKS`, `compile.ts:87-90`) |
650
- | `physics.inertia/strength/damping/mass/wind/gravity` | ❌ |
661
+ | `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` |
651
662
  | `ik`, `transform` | ✅ — one unnamed timeline per constraint, keyed by the motion spec's `ik` / `transform` arrays |
652
663
  | `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 |
653
664
  | `attachments.<skin>.…deform` | ❌ (validator knows it: 1 channel, `validate.ts:104-107`) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.22.2",
3
+ "version": "0.24.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": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ingest
3
- description: Work with a Spine skeleton.json somebody else authored — exported from the Spine editor or another tool — using rigc. Read and validate it, understand a complaint rigc raised about it, transcribe it into rigc specs, normalise, re-pivot or rename it, and extend it with an animation it does not have. Use when the input is an existing skeleton.json with its .atlas and page images rather than loose part PNGs. Not for Live2D file conversion or runtime tracking.
3
+ description: Work with a Spine skeleton.json somebody else authored — exported from the Spine editor or another tool — using rigc. Read and validate it, understand a complaint rigc raised about it, decompile it into rigc specs with `rigc ingest`, normalise, re-pivot or rename it, and extend it with an animation it does not have. Use when the input is an existing skeleton.json with its .atlas and page images rather than loose part PNGs. Not for Live2D file conversion or runtime tracking.
4
4
  license: MIT
5
5
  compatibility: Requires Bun 1.2 or later and the npm package spine-rigc.
6
6
  ---
@@ -17,26 +17,48 @@ not do for you.
17
17
 
18
18
  - **Validation is never bypassed.** `validate` reads a foreign file as it is, and
19
19
  `build` writes nothing on a red gate — AUTHORING §0.
20
- - **The compiler never invents a value.** A transcription states every bone, slot
21
- and key the specs need; what the export left implicit has to be written down
22
- before it compiles — INGEST §2.
20
+ - **The compiler never invents a value, and neither does the decompiler.** The
21
+ specs state every bone, slot and key; what the export left implicit is written
22
+ down before it compiles. `rigc ingest` obeys the same rule from the other side —
23
+ what it cannot read out of the skeleton is a named **finding**, never a guess —
24
+ INGEST §2.0 and §2.
23
25
  - **The validator's messages are the instructions.** A red line on an export is a
24
26
  fact about the file, and sometimes about the rule — INGEST §3 says which, and
25
27
  AUTHORING §5 names the file to change.
26
28
 
29
+ ## Start here: `rigc ingest`
30
+
31
+ ```bash
32
+ rigc ingest hero.json --out specs/ --stage 0,0,1024,768
33
+ rigc build --rig specs/rig.json --motion specs/motion.json --images parts/ --out build/
34
+ ```
35
+
36
+ It reads the skeleton — **only** the skeleton — and writes the rig spec and motion
37
+ spec that rebuild it, byte for byte. Two values are not in the file and it refuses
38
+ rather than guessing them: the **stage** (`--stage`, an editor export carries none)
39
+ and each animation's **duration** (the largest key time, recorded as a finding).
40
+ Read `findings.json`: a `BLOCK` line means the rebuild will be missing something and
41
+ the command exits non-zero. Keep the `note` both specs carry. INGEST §2.0.
42
+
27
43
  ## What this guide will not do
28
44
 
29
- rigc cannot write a skeleton back. There is no command that edits a `skeleton.json`
30
- and no route from it to specs, so every change goes through transcription — INGEST
31
- §0 and §2. `diff`'s ratios say how much of a reference's structure a candidate
32
- reproduces, so extending or renaming a foreign skeleton lowers them by design, and
33
- neither `validate` nor `diff` has a pass bar — INGEST §0 and §4.
45
+ rigc still cannot **edit** a skeleton: there is no command that opens one and
46
+ changes it, so every change is expressed in the specs — INGEST §0. `diff`'s ratios
47
+ say how much of a reference's structure a candidate reproduces, so extending or
48
+ renaming a foreign skeleton lowers them by design, and neither `validate` nor `diff`
49
+ has a pass bar — INGEST §0 and §4.
50
+
51
+ ⚠️ This section said *"there is no route from it to specs, so every change goes
52
+ through transcription"* until 2026-09-17. That route exists now and it is the first
53
+ thing to reach for; transcription by hand is what you fall back on for a construct
54
+ `ingest` reports as a blocker.
34
55
 
35
56
  ## Read, in this order
36
57
 
37
58
  1. [INGEST.md](../../docs/INGEST.md) — what every command will and will not do with
38
- a foreign file (§0), transcription (§2), what each validator complaint means on
39
- an export (§3), and the re-pivot, rename and extend recipes (§4).
59
+ a foreign file (§0), `ingest` and transcription (§2), what each validator
60
+ complaint means on an export (§3), and the re-pivot, rename and extend
61
+ recipes (§4).
40
62
  2. [AUTHORING.md](../../docs/AUTHORING.md) — the two spec files the transcription
41
63
  targets (§3–§4), the failure map (§5–§6), and the coordinate contract (§11).
42
64
  3. Then [RIGGING.md](../../docs/RIGGING.md) for why the re-pivot edit has the shape