@rig-c/core 2.82.0 → 2.82.2

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/AUTHORING.md CHANGED
@@ -20,7 +20,7 @@ and has no way to answer. This is not a caveat: a build with **every easing in t
20
20
  file reversed** gates green. If you were given pictures, `check` (**§9**) is the half of the loop
21
21
  that can see that, and a run that skips it has verified nothing about the motion.
22
22
 
23
- - Formats and CLI reference: [README.md](../README.md)
23
+ - Formats and CLI reference: this guide, starting with [§0](#0-the-loop)
24
24
  - The rig spec's own source-level documentation: [`src/rig.ts`](../src/rig.ts)
25
25
  - The motion spec and emitted shapes: [`src/types.ts`](../src/types.ts)
26
26
  - What the motion spec's own parser proves, and which refusals it deliberately
@@ -113,6 +113,97 @@ can see where Spine stops.
113
113
 
114
114
  ## 0. The loop
115
115
 
116
+ Every command takes its paths explicitly. `rigc <command> --help` prints its flags —
117
+ `rigc help <command>` prints the same page — and
118
+ [AUTHORING.md §0](../docs/AUTHORING.md) is the same list with what each flag means, which
119
+ commands take it and what its default is.
120
+
121
+ | Command | Does |
122
+ | --- | --- |
123
+ | `build --rig … --motion … --out …` | compiles, gates, and **writes only if the gate is green**. `--images <dir>` says where the rig spec's `image` names resolve, `--manifest` adds measured art, and `--copy-images` copies every page PNG into `--out` so the directory is self-contained, with `skeleton.images` pointing at it so the editor's import finds the parts <a id="cmd-build"></a> |
124
+ | `build … --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` — losslessly, and gated a second time as the pair that ships. `--page-size`, `--padding`, `--page-edges` and `--pack-shape` tune it — `--pack-shape polygon` lets a neighbour sit inside a mesh's rectangle where its hull is not, and with `--page-edges free` costs 12–56 times `rect`'s pack (3–11 s against 0.2–0.3 s on 90–144 parts over two to four pages; past 5 s from about 60 parts, a second or so below 45 on one page — AUTHORING §0.1) |
125
+ | `build … --atlas-in <file.atlas>` | the same build with every part resolved to a **region of an existing pack** instead of a loose PNG; a name the atlas lacks, a size the spec disagrees with or a rectangle off its page is refused by name |
126
+ | `validate <dir>` | re-gates artifacts already on disk <a id="cmd-validate"></a> |
127
+ | `repack <dir> --out <dir>` | a packed build **repacked from its own output** — `skeleton.json`, `skeleton.atlas` and the pages, with no parts kept — under the packing flags `build --pack` takes, so a packer improvement reaches a build whose parts are gone. Every region is lifted off its page, the skeleton read back with `ingest`, and `build --pack` run over the lifted parts with the same gate; it writes only after every region is shown pixel-identical to the input's, the skeleton byte-identical and the gate green, and refuses by name an atlas it cannot lift exactly. A skeleton the rebuild does not reproduce byte for byte — a build written before 2.2.0, whose header box was its stage — is refused naming every differing path, and written, every path printed, only under `--accept-skeleton-differences`. `--stage-box <slot>` reads the stage from the box a build carries for it (`skeleton.stageBox`), the one way the three files alone carry the stage exactly ([AUTHORING §0.4](../docs/AUTHORING.md)) <a id="cmd-repack"></a> |
128
+ | `repack <dir> --out <dir> --keep-pages` | **`skeleton.model.json` alone, for the pages already in place** — for a player that reads the document beside an atlas and pages that already shipped. The same read and stage rules, then `build --atlas-in` over the input atlas in place of the lift and the pack; it writes the document, and nothing else, only after its `pages` are shown to state the atlas as it stands, the rebuilt skeleton byte-identical to the one on disk (so `spine.sha256` names it — no acceptance flag here) and the gate green. `--out` is usually the build's own directory and must not already hold a `skeleton.model.json`; a page or region line the document has no field for is refused by name ([AUTHORING §0.4](../docs/AUTHORING.md)) |
129
+ | `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all <a id="cmd-ingest"></a> |
130
+ | `split --rig … --motion … --slot <name> --out <dir>` | the answer to a mesh refused because its triangles are **several pieces that share no vertex** — two legs after the triangles bridging them were deleted. It reads the two specs and writes them back into an absent or empty `--out` with one mesh per piece: piece 1 stays in the slot, piece k gets a new slot `<slot>_<k>` after it, each with the original's uvs and weights unchanged, its triangles renumbered outline first where the hull rule needs it, and every deform key re-sliced to the new indices — plus `split.json`, where every original vertex and triangle went. One piece is said so and nothing is written; what it cannot carry is refused by name ([AUTHORING §3.4.11](../docs/AUTHORING.md)) <a id="cmd-split"></a> |
131
+ | `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong <a id="cmd-explain"></a> |
132
+ | `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings ([AUTHORING §8.8](../docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--extremes` adds `extremes.json` and a native six-panel `extremes.png` (setup, geometry-pair min/max and three premultiplied RGBA diffs with a legend), plus one `EXTREMES` line per set. Per-channel minima/maxima and the farthest original-vertex geometry pair are distinct readings; valid channel extrema survive a geometry/image SKIP, with channel refusals reported separately; still or uncomparable geometry reports a named SKIP without changing render exit status. `--extremes-only` writes the same `extremes.json` and readable `extremes.png` bytes while skipping ordinary frame rasterization/encoding, `contact.png` and `frames.json`; it implies `--extremes` and takes no other diagnostic or sheet output flags. Look at min, max and diff before a person does ([AUTHORING §8.11](../docs/AUTHORING.md#811-min-max-and-diff-before-a-person-looks--render---extremes)). `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene. `--sheet [--sheet-format json-hash] [--page-edges pot\|free]` also packs each set's straight-alpha transparent frames into `sheet.png` + `sheet.json` (TexturePacker JSON Hash with a shared world-origin `anchor` and `2`-px clamped edge extrusion, verified through `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`) and `sheet-manifest.json` (`rigc-sheet/1`), leaving diagnostic `f####.png` and `contact.png` unchanged ([AUTHORING §8.10](../docs/AUTHORING.md#810-sprite-sheets-from-a-render--render---sheet)) — a self-contained sprite object per animation set; when animations play and how a scene is composed remains the consumer's <a id="cmd-render"></a> |
133
+ | `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` <a id="cmd-preview"></a> |
134
+ | `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` <a id="cmd-vote"></a> |
135
+ | `pose --images <dir> --frame <png>` | reads part placements **out of** a picture <a id="cmd-pose"></a> |
136
+ | `chainfit --candidate <dir> --images <dir> --frame <png>` | reads the parts `pose` refuses, through the candidate's own draw order and hierarchy: masked residuals over **visible** pixels, one hinge per child instead of four degrees of freedom, and the `rotate` key value each answer implies. A bone with two or more anchored descendants is **determined** rather than searched, and the residual that over-determination leaves is reported <a id="cmd-chainfit"></a> |
137
+ | `diff <candidate.json> <reference.json>` | structural comparison of two skeletons, one ratio per measure and deliberately no combined score <a id="cmd-diff"></a> |
138
+ | `bonedist --candidate … --reference … --bones …` | per-frame, per-bone world-transform distance against another skeleton — the ladder's stage 3, run on its own. `--bones <correspondence.json \| identity>` is required rather than defaulted: a candidate is entitled to its own bone names, so the pairing is stated <a id="cmd-bonedist"></a> |
139
+ | `check --candidate <dir> --frames <dir> [--out <dir>]` | the candidate against reference pictures — the only instrument here that can see a *wrong animation*. `--out` also writes, for every frame the table lists, the picture its figures came from: reference, candidate, difference and overlay at native size <a id="cmd-check"></a> |
140
+ | `bench <rung> --candidate <dir>` | one rung of the benchmark ladder <a id="cmd-bench"></a> |
141
+ | `guide <RULE>` / `guide §<n>[.<m>[.<k>]] [--all]` | the guide's row for the rule a failure line names — every `FAIL` line, `vote --record` refusal, `ingest` finding and coded compile error ends with ` (see rigc guide <RULE>)`; only legacy uncoded library errors point at `§5.1` — or a section's heading, own prose and subsection index, read from the installed `docs/AUTHORING.md` when it runs. `--all` includes the subsection bodies; a leaf prints its full text either way. Sizes describe full ledger spans. `--all` with a rule is refused, exit 2; an unknown name is refused with the nearest real ones, exit 1 <a id="cmd-guide"></a> |
142
+ | `skills install [--dir …] [--copy]` | links every agent skill the package ships into `.agents/skills` (or `--dir`) as relative symlinks, or copies them with `--copy` — where Codex, Gemini CLI and Antigravity look. A second run has nothing to do; an entry already there that is not this command's is refused by name and nothing is written <a id="cmd-skills-install"></a> |
143
+ | `review <dir> […]` | the human review of a build, handed to the `rig-review` bin of `@rig-c/review` installed beside the package; rigc does not depend on it and never installs it. `rigc review <dir> [--reference <video>] [--animation <name>] [--compare <dir>]` is `rig-review open <dir> …` with those flags 1:1 and prints the loopback URL to give a person; a first argument that is one of review's own words (`open`, `check`, `feedback`, `control`, `strip`, `compare`) or starts with `-` (`--help`, `--`) is handed over untouched. stdout is review's byte for byte, stderr review's plus the launcher's own refusals, the exit code review's. Refused by name, exit 2, before the bin starts: `REVIEW_OUTPUT_MISSING <path>` (the target is not a directory); `REVIEW_NOT_INSTALLED` with two lines, `bun add -d @rig-c/review` and `bunx @rig-c/review open <dir> …` carrying the flags given — the same on a terminal and off, with nothing installed or written; `REVIEW_BUN_TOO_OLD: Bun <found>, @rig-c/review needs <required>`, the floor read off the installed package's `engines.bun`; and an empty argument (`""`) under a Bun that drops one (Bun 1.2.0, measured by a probe), `rigc review: argument N is empty, and Bun <version>, the Bun running this, drops an empty argument when it starts a process — …`. docs/AUTHORING.md lists every line <a id="cmd-review"></a> |
144
+
145
+ `diff`, `bonedist`, `check` and `bench` measure against something you were given; the
146
+ first three work on any reference you have, and `bench` is a repository workflow that needs a clone
147
+ and `bun run fetch-examples`. The reasoning behind them is in
148
+ [the benchmark dossier](https://github.com/rig-c/core/blob/main/docs/BENCHMARK.md).
149
+
150
+ `build` and `validate` both default to `--profile spine` — the 36 validity rules, which
151
+ ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile rig-play`
152
+ adds all 50: the other 14 are one renderer's policy and one canvas budget's, and they
153
+ fire on perfectly correct editor-produced Spine data, which is why they are opt-in.
154
+ ⇒ **That reason is about foreign data and does not carry to a rig you are authoring
155
+ yourself: author under `--profile rig-play` and read the extra 14 as findings, and
156
+ gate the release under `--profile spine`.** A `deform` key that folds a mesh inside
157
+ out is written out under the default and refused by name under `rig-play`, which
158
+ is the shape of what that split buys you. A report always names
159
+ the profile it ran and lists what that profile left out.
160
+
161
+ Several cuts can also be registered in a `cuts.json` and built by name
162
+ (`build --cut my_cut --cuts path/to/cuts.json`); every path in that table resolves
163
+ relative to the `cuts.json` file itself, so the table lives with the project that owns
164
+ the art. Its shape is under
165
+ [Usage](https://github.com/rig-c/core/blob/main/docs/BENCHMARK.md#usage).
166
+
167
+ **`render`** samples every animation at 12 fps and writes `render/<animation>/f0000.png…`
168
+ with a `contact.png` beside them — every frame of the shot as one labelled grid,
169
+ which is the picture to open first, because spacing is a comparison *across*
170
+ frames. It draws with rigc's own rasteriser (the one `check` measures with), so
171
+ it needs no browser and no network, and the `frames.json` it leaves beside the
172
+ directories makes the result a frame set like any other — the world box every
173
+ frame is a picture of. `--animation <name>` narrows it to one, `--fps` and
174
+ `--max` change the rate and the frame size. A set drawn at any rate but the
175
+ default is written to `<anim>@<fps>fps/` rather than `<anim>/`, so two rates sit
176
+ side by side: `--fps 24` writes `<anim>@24fps/`, and the default 12 writes
177
+ `<anim>/`. Each set's summary line ends with the directory it wrote.
178
+
179
+ > 🚨 **`--skin <name>` if your rig has more than one.** With no `--skin` no skin
180
+ > is set at all, so every slot resolves through the **default** skin alone — and
181
+ > a slot whose art lives only in a named skin draws *nothing*. That is not a
182
+ > quirk of `render`: `check` compares `render`'s frames, so a multi-skin rig
183
+ > checked with no skin compares blank against blank and reports a perfect
184
+ > `0.0000` about art nobody drew. `render --skin` records the name in
185
+ > `frames.json`, `check --skin` poses the candidate under it, and a candidate
186
+ > scored against frames rendered under a *different* skin is refused by name
187
+ > rather than measured.
188
+
189
+ **`preview`** writes a single self-contained `.html`: your skeleton, your atlas
190
+ and every page's PNG bytes are embedded in it as data URIs, and it plays them in
191
+ the **official [Spine Web Player](https://esotericsoftware.com/spine-player)**.
192
+ Double-click it, or attach it to a message — the file carries the whole artifact.
193
+ It is also the strongest interop statement in this repository: a rig that plays
194
+ there has been played by Esoteric Software's own runtime rather than by ours.
195
+
196
+ > ⚖️ The player itself is **referenced, not embedded** — the page loads it from
197
+ > unpkg, so the first open needs a network, and rigc redistributes nothing
198
+ > Esoteric Software owns (see [NOTICE.md](../NOTICE.md)). Everything the player
199
+ > draws is inside your file.
200
+
201
+ Further repository material:
202
+
203
+ | Document | For |
204
+ | --- | --- |
205
+ | 🔬 [SURVEY_2026-08-22.md](https://github.com/rig-c/core/blob/main/docs/SURVEY_2026-08-22.md) | the survey the ladder was climbed by: rigc and the official examples as measured on 2026-08-22, and the gap list ordered by rung. A dated record, not kept current. Repository material |
206
+
116
207
  ```bash
117
208
  bun install # once
118
209
 
@@ -11935,7 +12026,7 @@ ambiguous one ends with a sentence saying which reading its figures support:
11935
12026
 
11936
12027
  - **below the floor** — a 40x28 part of one flat colour, cut from a plain block it fits anywhere inside:
11937
12028
 
11938
- > part is 40x28 px (span 40 frame px, opaque 1) with texture 0, detail 0; 17 candidate(s), best 0.0000, next 0.0000, spread 0.0000 — below the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up): pose placed 7% of the measured parts this plain, so this part is too plain for pose to tell its placements apart; that says nothing about whether the cut is right
12029
+ > part is 40x28 px (span 40 frame px, opaque 1) with texture 0, detail 0; 34 candidate(s), best 0.0000, next 0.0000, spread 0.0000 — below the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up): pose placed 7% of the measured parts this plain, so this part is too plain for pose to tell its placements apart; that says nothing about whether the cut is right
11939
12030
 
11940
12031
  - **above the floor** — a 200x150 black-and-white checker the frame does not hold:
11941
12032
 
@@ -11943,7 +12034,7 @@ ambiguous one ends with a sentence saying which reading its figures support:
11943
12034
 
11944
12035
  - **above the floor, one size in two places** — two copies of an 18x34 checker 0.6 of its span and 6° apart, the shape a left/right pair cut from one drawing reads as:
11945
12036
 
11946
- > part is 18x34 px (span 36.2 frame px, opaque 1) with texture 0.0221, detail 0.752; 14 candidate(s), best 0.0160, next 0.0162, spread 0.0002 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; every placement in the margin is at the best's scale (within 0.30%, bar 2%) and in another place (the nearest 0.62 span(s) away, bar 0.45), which is the part — or a near-copy, such as a left/right pair cut from one drawing — drawn more than once: read the placements as an unordered set and order it across frames (MOTION.md §2.2 🔀); a --rotation window that excludes one place chooses the answer rather than measuring it
12037
+ > part is 18x34 px (span 36.2 frame px, opaque 1) with texture 0.0221, detail 0.752; 40 candidate(s), best 0.0160, next 0.0162, spread 0.0002 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; every placement in the margin is at the best's scale (within 0.30%, bar 2%) and in another place (the nearest 0.62 span(s) away, bar 0.45), which is the part — or a near-copy, such as a left/right pair cut from one drawing — drawn more than once: read the placements as an unordered set and order it across frames (MOTION.md §2.2 🔀); a --rotation window that excludes one place chooses the answer rather than measuring it
11947
12038
 
11948
12039
  Read the figures, not only the verdict. *Below* says plainness alone explains the
11949
12040
  failure: the cut may be right, and `pose` cannot tell. *Above* says it does not,
@@ -12033,6 +12124,22 @@ two anchors — and only 3 had it ranked under the old cutoff of three. The fine
12033
12124
  took the grid from 185 to 227 trials found and those misses from 65 to 14, for about
12034
12125
  10–14% more CPU over the grid (615 s of user time before it, 678 s and 704 s on two runs after).
12035
12126
 
12127
+ For repeated parts, an independent coverage lane also keeps up to five **new**
12128
+ places per rung. It uses the same fields and stride, but bounds suppression by
12129
+ half the material box's **short** side at that rung's scale: a long thin part's
12130
+ half-length cannot erase a different nearby position. Positions already covered
12131
+ by the original lane do not consume that extra budget. The coverage lane keeps
12132
+ its coarse pool through the first finer, rotation-regridded level before comparing
12133
+ scales together; only then does it narrow to the usual refinement budget. Both
12134
+ completed pools use the same full-resolution objective, dedupe and ambiguity
12135
+ margin. More coverage can expose an ambiguity; it does not prove that every
12136
+ visible copy, or every subpixel placement, will be recovered. Public repeated-part
12137
+ controls hold the final reported position, rotation and scale, not merely a coarse
12138
+ seed. The programmatic `coarsePolicy: 'legacy-only'` seam reproduces the original
12139
+ search for those negative controls; the CLI uses short-support coverage. An unknown
12140
+ runtime `coarsePolicy` is refused by name with the supplied value and both accepted
12141
+ policies; omit it to use the current search.
12142
+
12036
12143
  The refinement then polishes each place down the pyramid, and at full resolution a
12037
12144
  polish that has stopped tries its scale **up by half a coarse rung** (2^(1/6)), re-fits
12038
12145
  the position there, and restarts if that scores better; fifteen places survive each
package/docs/FACE.md CHANGED
@@ -1947,7 +1947,9 @@ turning, blinking and looking — and the one general lesson that might carry in
1947
1947
  that work is §7's: **allocate the channels before the first key, because Spine
1948
1948
  blends and does not add.**
1949
1949
 
1950
- ### A measured alpha aperture
1950
+ <a id="a-measured-alpha-aperture"></a>
1951
+
1952
+ ### Depth-driven rigging: a measured alpha aperture
1951
1953
 
1952
1954
  A named socket plate can supply an `alphaClip` clipping attachment (AUTHORING,
1953
1955
  **Alpha-derived clipping**). This also accepts a generated grid/contour source
package/docs/INGEST.md CHANGED
@@ -20,7 +20,7 @@ reproduces* — so deliberately extending or renaming a foreign skeleton **lower
20
20
  them**, by design (§4.2, §4.3). Neither one has a pass bar, and this page does not
21
21
  invent one.
22
22
 
23
- - Formats and the CLI reference: [README.md](../README.md)
23
+ - Formats and the CLI reference: [AUTHORING §0](AUTHORING.md#0-the-loop)
24
24
  - The rig spec, field by field: **AUTHORING §3**, and [`src/rig.ts`](../src/rig.ts)
25
25
  - The motion spec, field by field: **AUTHORING §4**
26
26
  - Named failures, and the file each one points at: **AUTHORING §5–§6**
@@ -1405,11 +1405,11 @@ rigc pose
1405
1405
  .. ground rgb(232, 232, 232) over 100% of the border ring
1406
1406
  .. parts …/packed-only (1 png)
1407
1407
  .. search scale 0.5–2 in 7 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
1408
- PLACE 3-timing-and-spacing.png x= 324.3 y= 188.3 rot= -91.6° scale=0.630 residual=0.2085 unexplained= 30%
1408
+ PLACE 3-timing-and-spacing.png x= 324.6 y= 189.8 rot= -91.1° scale=0.625 residual=0.2080 unexplained= 29%
1409
1409
  found on a 14x7 anchor grid, step 4 at 16x reduction
1410
1410
  ```
1411
1411
 
1412
- ⚠️ **`residual=0.2085` is *under* the default 0.25 refusal bar**, so nothing refused
1412
+ ⚠️ **`residual=0.2080` is *under* the default 0.25 refusal bar**, so nothing refused
1413
1413
  it, and `PLACE` rather than `AMBIG` means nothing flagged it either. With the same frame
1414
1414
  and the two real loose parts, the answer is what it should be:
1415
1415
 
@@ -1424,7 +1424,7 @@ rigc pose
1424
1424
  .. ground rgb(232, 232, 232) over 100% of the border ring
1425
1425
  .. parts …/examples/3-timing-and-spacing/images (2 png)
1426
1426
  .. search scale 0.3–0.6 in 4 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
1427
- PLACE pendulum.png x= 319.3 y= 214.6 rot= -88.9° scale=0.411 residual=0.0428 unexplained= 7%
1427
+ PLACE pendulum.png x= 319.4 y= 214.4 rot= -88.9° scale=0.412 residual=0.0421 unexplained= 7%
1428
1428
  found on a 28x13 anchor grid, step 2 at 16x reduction
1429
1429
  PLACE square.png x= 437.1 y= 343.1 rot= 0.0° scale=0.410 residual=0.0334 unexplained= 4%
1430
1430
  found on a 113x51 anchor grid, step 2 at 4x reduction
@@ -1490,3 +1490,142 @@ The transcription these recipes start from is
1490
1490
  unmodified. The re-pivoted, renamed and extended variants in §4 were built from copies
1491
1491
  of it; none is committed, because each is an illustration of an edit rather than a
1492
1492
  transcription of anything.
1493
+
1494
+ ### Starting from a skeleton you already have
1495
+
1496
+ `rigc ingest` reads a Spine 4.3 `skeleton.json` and writes the two spec files that
1497
+ rebuild it. It is the only command that runs against `build`'s direction, and the
1498
+ only one whose contract is an equality rather than a rulebook:
1499
+
1500
+ ```bash
1501
+ rigc ingest hero.json --out specs/ --images parts/
1502
+ rigc build --rig specs/rig.json --motion specs/motion.json --out build/
1503
+ rigc diff build/skeleton.json hero.json
1504
+ ```
1505
+
1506
+ `--images parts/` is what makes the second line carry no flag: it writes the spec's
1507
+ own `images` directory, spelled from `--out`, so the specs are self-contained from
1508
+ there on. Leave it off and the `image` names resolve against `specs/` itself, which
1509
+ holds no art — every rebuild then has to repeat `build --images parts/`.
1510
+
1511
+ **`build(ingest(x))` is `x`.** Over the eleven rigs this repository builds — the seven
1512
+ gallery examples, the three generated probes and a coverage probe written for the
1513
+ purpose — the rebuilt `skeleton.json` is byte for byte the file the decompiler read.
1514
+ The atlas is held to a weaker
1515
+ claim on purpose, and the weakening is measured rather than assumed: it comes back
1516
+ equal as a **multiset of region blocks**, because the order the pages are collected in
1517
+ is in no field of the skeleton.
1518
+
1519
+ **And over twelve skeletons nobody here wrote.** Every editor export in the fetched
1520
+ example corpus, ingested, rebuilt through the pack beside it and `diff`ed against the
1521
+ source, comes back **12 of 12, no blockers, 1.000 on every measure the report
1522
+ carries** — and each rebuild is its export's own text in canonical form, apart from
1523
+ the header's `hash` and `spine`: the editor's project hash and the runtime version
1524
+ rigc stamps, the two keys the rig spec has no field for by design (12 of 12) — and
1525
+ the header's box, which `build` computes rather than carries (issue #907): the rebuild
1526
+ writes spine-core's `getBounds` over the export on the header's 1e-6 grid at float32, equal on all twelve,
1527
+ where the editor wrote its own arithmetic, up to 0.0071 units away.
1528
+ [INGEST.md §2.3](../docs/INGEST.md) states that pass line and why those two are the
1529
+ exceptions.
1530
+
1531
+ **What it reads is skeleton JSON and nothing else** — no `.spine` project, no binary
1532
+ `.skel`, no atlas, no art. So it never invents, and the things it cannot get out of
1533
+ the file are **findings** with codes rather than plausible values: a construct the
1534
+ spec format cannot hold (`point`, a `sequence` block, an unknown field
1535
+ on a bone, slot or constraint) is a blocker, the command exits non-zero, and both
1536
+ specs are still written — a spec plus a list of what is missing from it beats no spec.
1537
+
1538
+ ⚠️ **Two values are not in a skeleton at all.**
1539
+
1540
+ - **The stage.** `skeleton.width`/`height`. A skeleton that declares none is carried as
1541
+ declaring none — the rig spec states `"width": null, "height": null` and the rebuild
1542
+ carries no box either, byte for byte — and `--stage x,y,w,h` is how a
1543
+ caller *adds* one, recorded as a judgement. It is not derivable — posing the rig gives
1544
+ the *animated* extent, which is a different number from the setup box. ⛔ **And `--stage` beside a box the file already states is refused
1545
+ too**, for the opposite reason: two sources for one value, where the file is the record
1546
+ of what was measured. **All twelve exports in the example corpus carry a stage** and
1547
+ none of them needs the flag. It is the value that costs least to get wrong, because
1548
+ `diff` reports the box and gates nothing on it. 🔁 The box an export's header carries
1549
+ is its setup-pose bounding box, and it becomes the rebuild's *stage*; the rebuild's own
1550
+ header is computed again, because since issue #907 `build` writes the setup-pose
1551
+ bounding box there — never the stage — held to spine-core's `getBounds`. 📦 A rig
1552
+ that has to carry its stage in the Spine files asks for it as a bounding box
1553
+ (`skeleton.stageBox`, written by `build` from the stage and held by
1554
+ `A50_STAGE_BOX_IS_THE_STAGE`), and `ingest --stage-box <slot>` reads that box back as
1555
+ the rebuild's stage — [AUTHORING §3.1](../docs/AUTHORING.md) has the field and the
1556
+ paragraph for a consumer of the files.
1557
+ - **An animation's duration.** The format has no such field. The largest key time is
1558
+ the only derivable answer and it is what a runtime plays to; it is wrong for an
1559
+ animation that holds its last pose past its last key, so it is recorded as a finding
1560
+ on every animation rather than chosen quietly.
1561
+
1562
+ Both specs carry a `note` that `ingest` writes itself, saying the file is decompiled
1563
+ and naming the skeleton it came from — because a decompiled spec is indistinguishable
1564
+ from an authored one by inspection, every gate here calls it green (it *is* green),
1565
+ and no gate can catch a missing note.
1566
+
1567
+ [docs/INGEST.md](../docs/INGEST.md) is the whole page on working from a file you were
1568
+ handed; [docs/AUTHORING.md](../docs/AUTHORING.md) §0.3 is the loop.
1569
+
1570
+ ### The editor round trip — for a licence holder, never in CI
1571
+
1572
+ `tools/editor_roundtrip.ts` drives the loop the output's whole premise rests on:
1573
+ build → **import into the Spine editor** → export back to JSON → gate, `diff`,
1574
+ `render` and `check` the export against the build it came from.
1575
+
1576
+ ```
1577
+ bun cli.ts build --rig … --motion … --out build/ --copy-images
1578
+ bun tools/editor_roundtrip.ts --build build/ --editor /Applications/Spine.app/Contents/MacOS/Spine
1579
+ ```
1580
+
1581
+ Of the editor's `--version` it records one line, the `Spine <x.y.z> …` line that
1582
+ carries the version, and nothing else that command prints — the rest of it
1583
+ names the licence holder — or `editor version: not found in --version output`
1584
+ where no such line is there. That call carries the same `-u` as the import and
1585
+ the export, so a trip pinned with `--editor-version <v>` names the editor it
1586
+ pinned and says so on the line (`(read from --version under -u <v>, …)`); an
1587
+ unpinned trip names the launcher's default. Once the export exists the version
1588
+ is held against the export's own `skeleton.spine`, and a disagreement is a FAIL
1589
+ naming both values — the line would otherwise name an editor the trip did not
1590
+ run on. It prints the import and export exit codes, the
1591
+ validator's verdict on the export, every `diff` measure that moved, `check`'s mean MAE and worst drift per
1592
+ animation **for each skin the build and the export both declare** — one
1593
+ render-and-check block per skin, with a per-skin roll-up under them, because a
1594
+ rig's contested art lives in its named skins and a single un-skinned check draws
1595
+ none of it; a skin only one side declares is a FAIL naming it as **lost** (or
1596
+ **added**) **by the export**, with `diff`'s `attachments.skins` beside it, and is
1597
+ rendered on neither side — and a
1598
+ field-by-field list of what the editor rewrote. Every step quotes what its child
1599
+ said when that child did not do what it was for, the renderers included — the
1600
+ editor's words in full and in order, except its `Licensed to:` line, which names
1601
+ the licence holder and is replaced by a line saying it was withheld; a skin
1602
+ **neither** side can draw — a hit-box rig, say — is a **SKIP** naming that, not a
1603
+ red, because `check` had nothing to compare and `diff` and `validate` have
1604
+ already measured the rig. One side drawing where the other does not is the
1605
+ divergence the trip exists to find and stays a failure. A human edit made in the
1606
+ editor survives the trip back.
1607
+
1608
+ 🔒 **It requires a licensed Spine editor on the machine, by construction**, and
1609
+ drives only the [documented command line](https://esotericsoftware.com/spine-command-line-interface)
1610
+ — never the UI, and it produces nothing the editor did not produce. With no
1611
+ editor present it refuses by name and exits non-zero, and so does the **trial**:
1612
+ the trial cannot save projects or export animation data, so the refusal names
1613
+ what it found — the executable, the bundle, the `CFBundleName` that bundle
1614
+ declares, or the banner the binary prints about itself — rather than starting it
1615
+ and failing downstream. Both refusals point at `--exported <file>`, which
1616
+ measures an export the editor already made and is the half of this tool that
1617
+ needs no editor at all. A build whose `skeleton.json` is missing, is not JSON, or
1618
+ holds JSON that is not one object is refused by name before the editor starts,
1619
+ and so is a build with no atlas or an atlas that is a directory, or an
1620
+ `--exported` file that is not one JSON object.
1621
+
1622
+ ⛔ **Run the round trip by hand, on a machine that has the editor.** CI has no
1623
+ editor, so no automated check runs it. The
1624
+ [pre-landing fixture policy](https://github.com/rig-c/core/blob/main/CLAUDE.md#the-selftest-and-its-fixtures)
1625
+ is the canonical explanation of which rigs the by-hand measurement covers and why.
1626
+
1627
+ ⚠️ Build with `--copy-images`. An ordinary build's atlas names its pages by a
1628
+ relative path back to the art directory, and the round trip copies that atlas to
1629
+ a directory at another depth — the tool refuses such a build by name rather than
1630
+ letting `A17` blame the editor for the harness's own doing.
1631
+
package/docs/MOTION.md CHANGED
@@ -818,20 +818,22 @@ rigc pose
818
818
  found on a 20x25 anchor grid, step 4 at 2x reduction
819
819
  PLACE flag.png x= 104.7 y= 156.1 rot= 84.3° scale=0.955 residual=0.0135 unexplained= 0%
820
820
  found on a 40x50 anchor grid, step 2 at 2x reduction
821
- AMBIG post.png x= 79.9 y= 149.0 rot= -0.1° scale=0.971 residual=0.0593 unexplained= 16%
821
+ AMBIG post.png x= 80.0 y= 148.6 rot= -0.1° scale=0.982 residual=0.0587 unexplained= 16%
822
822
  found on a 14x17 anchor grid, step 6 at 2x reduction
823
- alt 2: x= 79.0 y= 141.7 rot= -0.5° scale=0.691 residual=0.0687 unexplained= 25%
824
- ambiguous: part is 14x96 px (span 93.2 frame px, opaque 1) with texture 0.0146, detail 1.406; 43 candidate(s), best 0.0593, next 0.0687, spread 0.0095 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; either the frame holds more than one place this part fits, or every candidate missed the true one — a narrower --scale or --rotation window around what you know of the part tells the two apart
823
+ alt 2: x= 78.7 y= 164.0 rot= -0.6° scale=0.573 residual=0.0612 unexplained= 19%
824
+ alt 3: x= 79.0 y= 141.7 rot= -0.5° scale=0.691 residual=0.0687 unexplained= 25%
825
+ alt 4: x= 78.4 y= 135.5 rot= -0.5° scale=0.500 residual=0.0702 unexplained= 23%
826
+ ambiguous: part is 14x96 px (span 94.3 frame px, opaque 1) with texture 0.0146, detail 1.406; 50 candidate(s), best 0.0587, next 0.0612, spread 0.0025 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; either the frame holds more than one place this part fits, or every candidate missed the true one — a narrower --scale or --rotation window around what you know of the part tells the two apart
825
827
  ```
826
828
 
827
- ⚠️ **The post came back `AMBIG`: a best at scale 0.971 and an alternate at 0.691, 7.3 px
828
- higher, trailing it by a spread of 0.0095.** That is §2.2's caveat in the open. The post
829
+ ⚠️ **The post came back `AMBIG`: a best at scale 0.982 and its next alternate at 0.573,
830
+ 15.4 px lower, trailing it by a spread of 0.0025.** That is §2.2's caveat in the open. The post
829
831
  is a long part with most of its area in one colour, so a shrunken copy sitting inside the
830
832
  real post explains those pixels nearly as well, and the report's own `ambiguous:` line
831
833
  says size and texture do not account for how close the two come. The report cannot say
832
- which one is true, and it says so: it names both and asks for a narrower window. The
834
+ which one is true, and it says so: it names the alternatives and asks for a narrower window. The
833
835
  picture can. The arm and the flag read 0.970 and 0.955, so the picture is at the art's
834
- own resolution, and a post at 0.69 would be drawn at about 0.7 of the size of everything
836
+ own resolution, and a post at 0.573 would be drawn at about 0.6 of the size of everything
835
837
  bolted to it. ⇒ Narrow, and say so:
836
838
 
837
839
  ```bash
@@ -861,12 +863,19 @@ rigc pose
861
863
  .. search scale 0.85–1.2 in 2 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
862
864
  PLACE arm.png x= 102.5 y= 94.8 rot= -18.3° scale=0.974 residual=0.0305 unexplained= 4%
863
865
  found on a 20x25 anchor grid, step 4 at 2x reduction
864
- PLACE flag.png x= 137.4 y= 86.0 rot= -6.6° scale=0.956 residual=0.0134 unexplained= 0%
866
+ PLACE flag.png x= 137.4 y= 86.0 rot= -6.4° scale=0.958 residual=0.0134 unexplained= 0%
865
867
  found on a 40x50 anchor grid, step 2 at 2x reduction
866
868
  PLACE post.png x= 80.0 y= 148.0 rot= 0.0° scale=0.995 residual=0.0394 unexplained= 9%
867
869
  found on a 14x17 anchor grid, step 6 at 2x reduction
868
870
  ```
869
871
 
872
+ The worked derivation and recipe below keep their recorded rounded authoring inputs:
873
+ pose A as printed here, and pose B with the flag at screen rotation −6.6° and scale
874
+ 0.956. The current rerun above reads −6.4° and 0.958 for that flag. The 0.2° rotation
875
+ and 0.002 scale differences are changes in the estimator's reading; the fixed inputs
876
+ used to derive this worked rig and its motion are stated here so that the recipe does
877
+ not silently change with a rerun.
878
+
870
879
  Three things to read out of that pair, none of which is a score:
871
880
 
872
881
  - **Scale.** All six readings sit in 0.956–0.995, a spread of about 4 %. The one part
@@ -887,7 +896,8 @@ Three things to read out of that pair, none of which is a score:
887
896
 
888
897
  ### 3. Convert, and derive the rig
889
898
 
890
- `cropToSpineY(y, 200) = 200 − y` and `screenToSpineDegrees(d) = −d`, both from
899
+ For those recorded authoring inputs, `cropToSpineY(y, 200) = 200 − y` and
900
+ `screenToSpineDegrees(d) = −d`, both from
891
901
  [`src/transform.ts`](../src/transform.ts) (§2.2 — do not open-code either):
892
902
 
893
903
  | | pose A, Spine world | pose B, Spine world |
@@ -1042,8 +1052,8 @@ which is which is worth being able to point at:
1042
1052
 
1043
1053
  | Key | Where it came from |
1044
1054
  | --- | --- |
1045
- | arm `t: 0` = −61.9, flag `t: 0` = −22.2 | **given** — pose A, converted. Untouched, per §3.6 |
1046
- | arm `t: 0.55` = 18.3, flag `t: 0.55` = −11.7 | **given** — pose B. The flag's is `6.6 − 18.3`: both world rotations converted first, then differenced, because a child's track carries a **local** rotation under a rotated parent |
1055
+ | arm `t: 0` = −61.9, flag `t: 0` = −22.2 | **given** — the recorded pose A reading, converted. Untouched, per §3.6 |
1056
+ | arm `t: 0.55` = 18.3, flag `t: 0.55` = −11.7 | **given** — the recorded pose B reading. The flag's is `6.6 − 18.3`: both world rotations converted first, then differenced, because a child's track carries a **local** rotation under a rotated parent |
1047
1057
  | arm `t: 0.07` = −66.4 | §3.6 — 4.5° against an 80° excursion (5.6 %), at 13 % of the duration |
1048
1058
  | arm `t: 0.32` = 24.7 | §3.8 — 6.4° past the end value (8 %), at 58 % of the duration |
1049
1059
  | flag `t: 0.09`, `t: 0.47` | §3.7 — the flag's extreme lands at 85 % of the duration against the arm's 58 %, an offset of **+27 %**, the middle of its row's band, and it drags the other way first. Not the top of the band: at 35 % (`t: 0.51`) the crossing and the settle below would share 0.04 s, and the 24 fps render would still show the flag at its extreme one frame before the last |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/core",
3
- "version": "2.82.0",
3
+ "version": "2.82.2",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -148,5 +148,7 @@ spec files field by field, the emission rules, the loop, and the failure map. Th
148
148
  Every guide linked here is in the installed package at `node_modules/@rig-c/core/docs/`,
149
149
  which is the copy that matches the rigc you run; the links go to the repository's
150
150
  `main`. Inside the Claude Code plugin the same files are at `${CLAUDE_PLUGIN_ROOT}/docs/`.
151
- Formats, the CLI reference and the licence chain:
152
- [README.md](https://github.com/rig-c/core/blob/main/README.md), installed at `node_modules/@rig-c/core/README.md`.
151
+ Formats and the CLI reference: [AUTHORING §0](https://github.com/rig-c/core/blob/main/docs/AUTHORING.md#0-the-loop),
152
+ installed at `node_modules/@rig-c/core/docs/AUTHORING.md`;
153
+ existing skeletons: [INGEST](https://github.com/rig-c/core/blob/main/docs/INGEST.md);
154
+ licensing: [NOTICE](https://github.com/rig-c/core/blob/main/NOTICE.md).