spine-rigc 0.15.0 → 0.17.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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "rigc",
3
+ "owner": {
4
+ "name": "firejune",
5
+ "url": "https://github.com/firejune"
6
+ },
7
+ "description": "rigc, the rig compiler for Spine, packaged as agent skills over the guides the npm package ships.",
8
+ "plugins": [
9
+ {
10
+ "name": "rigc",
11
+ "source": "./",
12
+ "description": "Author Spine 4.3 rigs with rigc: an entry skill with the install line and the build, validate, render, check and vote loop, plus routers for rigging, motion, faces and foreign skeletons, each linking the shipped guide that owns its rules.",
13
+ "homepage": "https://github.com/firejune/rigc#readme",
14
+ "repository": "https://github.com/firejune/rigc",
15
+ "license": "MIT",
16
+ "keywords": ["spine", "spine2d", "skeletal-animation", "rig", "compiler", "ai-agents"]
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
+ "name": "rigc",
4
+ "description": "Author Spine 4.3 rigs with rigc, the rig compiler that verifies its own output through a spine-core round-trip. Skills that route an agent to the shipped guide for the need at hand: the hierarchy, a motion, a face, or a skeleton somebody else authored.",
5
+ "author": {
6
+ "name": "firejune",
7
+ "url": "https://github.com/firejune"
8
+ },
9
+ "homepage": "https://github.com/firejune/rigc#readme",
10
+ "repository": "https://github.com/firejune/rigc",
11
+ "license": "MIT",
12
+ "keywords": ["spine", "spine2d", "skeletal-animation", "rig", "compiler", "ai-agents"]
13
+ }
package/LICENSE CHANGED
@@ -19,3 +19,12 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
+
23
+
24
+ THIRD-PARTY MATERIAL
25
+
26
+ The licence above covers rigc's own code, documentation and art. It does not cover
27
+ third-party material distributed alongside them - in particular the rendered
28
+ reference frames under bench/reference/, which carry Esoteric Software's example
29
+ image data and are governed by each example's own license.txt beside them:
30
+ redistribution only with that file, and no commercial use. See NOTICE.md.
package/NOTICE.md CHANGED
@@ -54,9 +54,11 @@ Two consequences worth stating plainly:
54
54
  ## Example assets
55
55
 
56
56
  The official Spine example projects are the yardstick this compiler is measured
57
- against. They are owned by Esoteric Software and are **not redistributed** in this
58
- repository: `scripts/fetch-examples.sh` downloads them into a gitignored
59
- `examples/` directory for local evaluation.
57
+ against. They are owned by Esoteric Software. **The example projects themselves are
58
+ not committed to this repository**: `scripts/fetch-examples.sh` downloads them into a
59
+ gitignored `examples/` directory for local evaluation. What this repository *does*
60
+ commit is its own rendered frames of them, under the grant the examples' own licence
61
+ files carry — *Rendered reference frames*, below.
60
62
 
61
63
  Each example directory upstream carries its own `license.txt`, so the terms are
62
64
  per-directory rather than repository-wide. Verified on 2026-08-22 against
@@ -87,7 +89,9 @@ the copyright line:
87
89
  So, for this repository's purposes:
88
90
 
89
91
  - **Images** — redistributable only with the accompanying `license.txt`, and
90
- **non-commercial only**. That is why they are fetched rather than committed.
92
+ **non-commercial only**. That is why the example projects are fetched rather than
93
+ committed, and why the rendered frames this repository *does* commit each carry a
94
+ verbatim copy of that file beside them — *Rendered reference frames*, below.
91
95
  - **Project files** (`.spine`, and the exports derived from them) — **public
92
96
  domain**, usable as the basis for derivative work. This is what makes the
93
97
  examples usable as a structural yardstick.
@@ -95,3 +99,31 @@ So, for this repository's purposes:
95
99
  grant its siblings carry does not exist for it — there is no licence file to
96
100
  accompany its images with. `scripts/fetch-examples.sh` prints a warning naming
97
101
  it. Treat its images as not redistributable.
102
+
103
+ ### Rendered reference frames
104
+
105
+ `bench/reference/` contains **1,293 PNG frames rendered by this project** from the
106
+ examples' own exports: `bench/render_reference.ts` loads each example's `export/`
107
+ out of the gitignored `examples/`, poses it with `spine-core`, and rasterises each
108
+ posed attachment.
109
+
110
+ A rendered frame contains those images' pixels, so committing one **is**
111
+ redistribution — and each example's `license.txt` grants exactly that, *"as long as
112
+ they are accompanied by this license file"*. So **a verbatim copy of the relevant
113
+ `license.txt` sits at each example root** under `bench/reference/`, put there by
114
+ `render_reference.ts` rather than left to memory; all eight are present. The
115
+ **non-commercial** condition in that same file rides along with those images, and
116
+ `LICENSE` says so — rigc's MIT grant covers rigc's own code, documentation and art,
117
+ and does not extend to this material.
118
+
119
+ ⚠️ **`7-anticipation` is excluded, and mechanically so.** With no upstream
120
+ `license.txt` there is no grant to rely on, so its frames are never committed:
121
+ `render_reference.ts` writes them only into a gitignored directory, refuses any
122
+ `--out` inside the repository that `git check-ignore` will not accept, fails closed
123
+ if git cannot answer, and drops a `LOCAL-ONLY.txt` beside them in place of the
124
+ licence file that does not exist.
125
+
126
+ `bench/reference/` is repository material — it is not in `package.json`'s `files`
127
+ list, so it is not part of the published npm package. The full reasoning, and the
128
+ per-rung framing behind it, is in
129
+ [`bench/reference/README.md`](https://github.com/firejune/rigc/blob/main/bench/reference/README.md).
package/README.md CHANGED
@@ -18,7 +18,7 @@ their own work.
18
18
  <img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/rigc-demo.gif" alt="Loose part PNGs assembling themselves into a character that breathes, blinks and waves" width="600" />
19
19
  </p>
20
20
 
21
- <p align="center"><em>Fourteen hand-drawn part PNGs, one rig spec, one motion spec — the assembly,
21
+ <p align="center"><em>Fourteen part PNGs drawn from scratch for this repo, one rig spec, one motion spec — the assembly,
22
22
  the breathing and the wave are all rigc-compiled Spine animations, rendered with
23
23
  <code>rigc render</code>.</em></p>
24
24
 
@@ -73,6 +73,22 @@ the following is a restatement of Esoteric Software's terms, not a term of ours:
73
73
 
74
74
  See [NOTICE.md](NOTICE.md) for the full notice.
75
75
 
76
+ 📐 **What of Esoteric Software's is in this repository, and under what grant.** No
77
+ example asset is committed: `bun run fetch-examples` downloads the example projects
78
+ into a gitignored `examples/`. What *is* committed is `bench/reference/` — 1,293 PNG
79
+ frames **this project renders** from those examples' own exports, so a frame carries
80
+ those images' pixels and committing one **is** redistribution. Each example's own
81
+ `license.txt` permits exactly that, *"as long as they are accompanied by this license
82
+ file"*, and a verbatim copy of it sits at each example root here — all eight, written
83
+ there by the render script rather than left to memory. The same file's
84
+ **non-commercial** condition rides along with those images, and
85
+ [LICENSE](LICENSE) says so: rigc's MIT grant covers rigc's own code, documentation
86
+ and art, not this material. `7-anticipation` publishes no `license.txt` upstream, so
87
+ no such grant exists for it and its frames are never committed — they render only
88
+ into a gitignored directory, enforced by `git check-ignore`. Full reasoning:
89
+ [`bench/reference/README.md`](https://github.com/firejune/rigc/blob/main/bench/reference/README.md)
90
+ (repository material, not in the npm package).
91
+
76
92
  The problem rigc is aimed at is narrow. An agent asked to author a rig has no way
77
93
  to tell whether it succeeded: Spine's JSON parser accepts a great deal of nonsense
78
94
  without a murmur — a constraint in the 4.2 shape simply vanishes, a `size:` that
@@ -114,10 +130,34 @@ the CLI in place); the two are interchangeable — `rigc build …` is
114
130
  `bun cli.ts build …`.
115
131
 
116
132
  Two commands are repository workflows rather than package ones: `bench` and
117
- `check` measure against Spine's official example projects and the reference
118
- frames rendered from them, which are fetched rather than redistributed (see
119
- [NOTICE.md](NOTICE.md)). They need a clone and `bun run fetch-examples`, and say
120
- so by name when the corpus is absent.
133
+ `check` measure against Spine's official example projects fetched, never
134
+ committed and against reference frames this project renders from them, which
135
+ **are** committed, each example's own `license.txt` beside them under the
136
+ redistribution grant those files carry; the images stay **non-commercial only**.
137
+ The reasoning is in
138
+ [`bench/reference/README.md`](https://github.com/firejune/rigc/blob/main/bench/reference/README.md)
139
+ and the terms in [NOTICE.md](NOTICE.md). Both commands need a clone and `bun run
140
+ fetch-examples`, and say so by name when the corpus is absent.
141
+
142
+ ### Install it into your agent
143
+
144
+ The guides under [Documentation](#documentation) also ship as
145
+ [Agent Skills](https://agentskills.io) — `skills/<name>/SKILL.md`, in this
146
+ repository and in the npm package — so an agent finds rigc the way it finds its
147
+ other tools. Each skill is a router and nothing more: when to load it, the
148
+ non-negotiables in a line apiece, and a link to the guide that owns every rule, so
149
+ a rule keeps living in exactly one place. The repository is also a Claude Code
150
+ plugin marketplace:
151
+
152
+ ```shell
153
+ /plugin marketplace add firejune/rigc
154
+ /plugin install rigc@rigc
155
+ ```
156
+
157
+ With the package already installed, `claude --plugin-dir node_modules/spine-rigc`
158
+ loads the same skills without a marketplace. The plugin carries no version of its
159
+ own — `/plugin update` follows `main` commit by commit, and the only version on
160
+ disk stays the one in `package.json`.
121
161
 
122
162
  ## First rig in ten minutes
123
163
 
@@ -410,8 +450,9 @@ was verified, and what writing it cost. Repository material: a clone and
410
450
 
411
451
  <p align="center"><em>The portrait rig playing its three animations in one take — the turn is
412
452
  the shot: both silhouette edges move apart, which a flat slide cannot do, because every
413
- feature carries its own depth. Scene direction of this kind was Live2D's territory; the
414
- split was authoring cost, and the cost is now one stated expression per key. Compiled and
453
+ feature carries its own depth. Scene direction of this kind is authorable on plain Spine
454
+ 4.3 no plugin, no runtime patch and the split was authoring cost rather than runtime
455
+ capability; the cost is now one stated expression per key. Compiled and
415
456
  rendered entirely by the published package.</em></p>
416
457
 
417
458
  🎞️ **How the three films on this page were made** is kept with them, one directory per
@@ -428,7 +469,7 @@ commands take it and what its default is.
428
469
 
429
470
  | Command | Does |
430
471
  | --- | --- |
431
- | `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 |
472
+ | `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 |
432
473
  | `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` and `--padding` tune it |
433
474
  | `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 |
434
475
  | `validate <dir>` | re-gates artifacts already on disk |
@@ -449,7 +490,7 @@ and `bun run fetch-examples`. The reasoning behind all three is in
449
490
 
450
491
  `build` and `validate` both default to `--profile spine` — the 25 validity rules, which
451
492
  ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
452
- adds all 39: the other 14 are one renderer's policy and one canvas budget's, and they
493
+ adds all 40: the other 15 are one renderer's policy and one canvas budget's, and they
453
494
  fire on perfectly correct editor-produced Spine data, so reach for that profile when
454
495
  you are shipping into *that* project rather than to be thorough. A report always names
455
496
  the profile it ran and lists what that profile left out.
@@ -471,7 +512,7 @@ the art. Its shape is under
471
512
  | 📥 **[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 |
472
513
  | 🤖 **[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 |
473
514
  | 🔬 **[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 |
474
- | 🎓 **[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 36 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 |
515
+ | 🎓 **[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 40 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 |
475
516
  | 📋 [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 |
476
517
 
477
518
  ## Why you can trust the output
@@ -481,11 +522,12 @@ rigc is measured against **Spine's own official example projects** — the
481
522
  as the graduation exam.
482
523
 
483
524
  🎓 **The ladder is complete, 2026-08-28.** All eight numbered rungs and the
484
- spineboy graduation exam are cleared and hold under the current gate, **v2.3**, every clause PASS or SKIP:
485
- worst attributable slot drift **5.55 px** against a 6.0 px bar, and **0 of 124**
486
- frame-change disagreements. Recompiling the same spec in a different session
487
- reproduced every field of the measurement record **to the digit**. The rungs stay
488
- in place as regression gates.
525
+ spineboy graduation exam are cleared and hold under the current gate, **v2.4**, every clause PASS or SKIP:
526
+ worst attributable slot drift **5.5550 px** against a 6.0 px bar a **1.0801×**
527
+ margin, the thinnest of the ladder's **G2** figures, and **G5**'s 1.0376× is thinner
528
+ still and **0 of 124** frame-change disagreements. Recompiling the same spec in a
529
+ different session reproduced every field of the measurement record **to the digit**.
530
+ The rungs stay in place as regression gates.
489
531
 
490
532
  🗓️ **One rung's pass was withdrawn and restored on 2026-09-02, and both are dated
491
533
  facts.** `check`'s extent tolerance ([PR #254](https://github.com/firejune/rigc/pull/254))
@@ -496,11 +538,15 @@ clause questions that exposed, as **v2.3**: a read-down names the framing of its
496
538
  evidence, and a slot whose attributability is **measured** to be capped below the bar
497
539
  reads down when everything observable about it is independently verified strict. That
498
540
  rung's third attempt clears on those grounds, on the candidate it already had.
499
- **Rungs 1–6 and 8 and the graduation exam were unaffected throughout**: each reproduces
500
- its gated figures to the digit, and the 5.55 px and 0-of-124 figures above are among
501
- them. Both verdicts, and the sweep of every candidate under the new gate, are in
541
+ **Rungs 1–6 and 8 and the graduation exam were unaffected throughout**: each keeps its
542
+ pass on the clause, and recompiling a stored candidate reproduces its record **to the
543
+ digit within one gate**. Across an instrument change the digits do move, and the
544
+ record says where — the graduation **G2** figure went 5.5491 → 5.5544 → **5.5550 px**
545
+ over the #301 sampler repair and v2.4's adoption of the re-rendered reference basis,
546
+ while **0 of 124** held throughout. Both verdicts, and the sweep of every candidate under the gate of the day, are in
502
547
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md)'s *PR #254 instrument re-inspection* and *gate-v2.3
503
- re-inspection*.
548
+ re-inspection*; the standing figures quoted above are from its *gate-v2.4
549
+ re-inspection*, which is the current sweep.
504
550
 
505
551
  ⚠️ **What that certifies, stated exactly.** That **the tool, the guide and the
506
552
  protocol reach the bar across a bounded series of honest attempts, each residual
@@ -509,6 +555,17 @@ predecessor's specs under the run protocol's inheritance clause. It is **not**
509
555
  that an agent authors a spineboy-scale rig from the brief alone in one run: the
510
556
  ladder has not demonstrated that, and each row records which of the two it is.
511
557
 
558
+ 🧪 **A separate series measures that harder question, and it has not been kind.**
559
+ From-zero attempts at spineboy — no inherited specs — have landed at **18.2, 18.8,
560
+ 19.57, 7.86, 9.33 and 18.98 px** worst drift against the 6.0 px bar, a spread with no
561
+ monotone trend, and the two most recent — both **2026-09-03**, after the ladder's
562
+ 2026-08-28 completion — are recorded 🔴 **FAIL**. They move no rung and reopen nothing —
563
+ a from-zero run is a tooling-progress measurement rather than a re-climb, which is
564
+ why the certification above is scoped to tool + guide + protocol. One of those
565
+ attempts states the residual in its own words: *"in motion it is not at editor
566
+ quality."* All six, with their verdicts, are in
567
+ [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
568
+
512
569
  The whole dossier — the yardstick, `diff` and `check` and what neither of them can
513
570
  see, every rung, the run viewer, the 40 assertions and the selftest behind them — is
514
571
  [docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
package/cli.ts CHANGED
@@ -114,6 +114,7 @@ import {
114
114
  } from './src/render.ts';
115
115
  import { CLI_DEFAULT_PROFILE, reportLines, validate, VALIDATE_PROFILES, type ValidateProfile } from './src/validate.ts';
116
116
  import { parseMotionSpec } from './src/motion.ts';
117
+ import type { FoldLimit, TurnCeiling } from './src/depth.ts';
117
118
  import type { CompileResult } from './src/types.ts';
118
119
 
119
120
  /**
@@ -407,10 +408,72 @@ function runGate(
407
408
  * unconditionally, so a build whose only mesh was a ribbon or a contour got a
408
409
  * sentence about a rim ring and a seam it does not have.
409
410
  */
411
+ /**
412
+ * What a depth map and a soft region put on a mesh, when it named either.
413
+ *
414
+ * The digests are the reason this prints at all: a claim about a rig can name
415
+ * WHICH sheet produced it, and two runs a reader believes differ can be shown to
416
+ * have read the same pixels. The ranges and counts are what say the input
417
+ * reached the geometry rather than merely being resolved — a `carried 0` never
418
+ * gets here (it is refused) and a `ramped 0` is a hard-edged mask, which is
419
+ * legal and usually not what somebody meant.
420
+ */
421
+ /**
422
+ * One axis's two ceilings, as `+31.41 / -18.03`, or what is unbounded on it.
423
+ *
424
+ * ⚠️ `none` and a number are different claims and are printed differently. A
425
+ * sheet with no gradient along an axis cannot fold anything on it AT ANY ANGLE,
426
+ * which is a fact about the sheet worth reading; printing `90` for it would be
427
+ * a limit nothing measured.
428
+ */
429
+ function ceilingPair(axis: { positive: FoldLimit | null; negative: FoldLimit | null }): string {
430
+ const one = (l: FoldLimit | null, sign: string) => (l === null ? `${sign}none` : `${sign}${l.degrees.toFixed(2)}°`);
431
+ return `${one(axis.positive, '+')} / ${one(axis.negative, '-')}`;
432
+ }
433
+
434
+ /** The tightest of the four, so the line that names a triangle names the right one. */
435
+ function tightestFold(c: TurnCeiling): { kind: string; sign: string; limit: FoldLimit } | null {
436
+ const all = [
437
+ { kind: 'yaw', sign: '+', limit: c.yaw.positive },
438
+ { kind: 'yaw', sign: '-', limit: c.yaw.negative },
439
+ { kind: 'pitch', sign: '+', limit: c.pitch.positive },
440
+ { kind: 'pitch', sign: '-', limit: c.pitch.negative },
441
+ ].filter((e): e is { kind: string; sign: string; limit: FoldLimit } => e.limit !== null);
442
+ if (all.length === 0) return null;
443
+ return all.reduce((best, e) => (e.limit.degrees < best.limit.degrees ? e : best));
444
+ }
445
+
446
+ function meshDepthNote(m: CompileResult['meshes'][number]): string {
447
+ const parts: string[] = [];
448
+ if (m.depth) {
449
+ parts.push(
450
+ `depth "${m.depth.image}" ${m.depth.digest} near=${m.depth.near} zScale=${m.depth.zScale} ` +
451
+ `z=[${m.depth.range[0]}, ${m.depth.range[1]}]`,
452
+ );
453
+ const c = m.depth.ceiling;
454
+ parts.push(`turn ceiling yaw ${ceilingPair(c.yaw)} pitch ${ceilingPair(c.pitch)}`);
455
+ const worst = tightestFold(c);
456
+ parts.push(
457
+ worst === null
458
+ ? ` nothing in this sheet folds: ${c.measured} triangle(s) measured, none with a depth gradient across it`
459
+ : ` first to fold: ${worst.kind} ${worst.sign} at ${worst.limit.degrees.toFixed(2)}°, ` +
460
+ `triangle ${worst.limit.triangle} [${worst.limit.ids.join(',')}]` +
461
+ `${c.degenerate ? `; ${c.degenerate} triangle(s) too flat in setup to measure` : ''}`,
462
+ );
463
+ }
464
+ if (m.soft) {
465
+ parts.push(
466
+ `soft "${m.soft.mask}" ${m.soft.digest} -> ${m.soft.bone}, ${m.soft.carried} carried / ${m.soft.ramped} in the falloff`,
467
+ );
468
+ }
469
+ return parts.length === 0 ? '' : `\n ${parts.join('\n ')}`;
470
+ }
471
+
410
472
  const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> = {
411
473
  ring: 'ring rim ring pinned on the window edge, seam ring pinned on the mask contour, aperture moves',
412
474
  ribbon: 'ribbon entry row pinned, rows share their weights so the strip lengthens without widening',
413
475
  contour: 'contour the art\'s own silhouette, every vertex pinned to the slot bone (geometry, not a deformation)',
476
+ grid: 'grid a lattice over the part window at stated column and row positions, every vertex pinned to the slot bone',
414
477
  authored: 'authored geometry rigc did not build; it assumes nothing about the topology',
415
478
  };
416
479
 
@@ -743,6 +806,10 @@ function cmdBuild(flags: Record<string, string>): void {
743
806
  'the loose part PNGs, which a packed atlas does not reference. Drop --copy-images',
744
807
  );
745
808
  }
809
+ // The copy itself happens after the gate (below). The header has to know NOW,
810
+ // because the skeleton text the gate reads is the skeleton text that is written
811
+ // — `skeleton.images` says where the parts will be (issue #370).
812
+ if (flags['copy-images'] !== undefined) opts.copyImages = true;
746
813
  // `--pack --profile spine-html` used to be the third refusal here, because
747
814
  // A06's coverage clause was "one part per page" flat and a legitimate pack
748
815
  // arrived at the gate reading as a defect. Since issue #266's second follow-up
@@ -792,7 +859,8 @@ function cmdBuild(flags: Record<string, string>): void {
792
859
  for (const m of result.meshes) {
793
860
  console.log(
794
861
  ` MESH ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
795
- `${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}`,
862
+ `${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}` +
863
+ meshDepthNote(m),
796
864
  );
797
865
  }
798
866
  for (const ph of result.physics) {
@@ -2219,9 +2287,14 @@ function cmdExplain(flags: Record<string, string>): void {
2219
2287
  console.log('\nmeshes');
2220
2288
  for (const kind of new Set(result.meshes.map((m) => m.kind))) console.log(` ${MESH_KIND_NOTES[kind]}`);
2221
2289
  for (const m of result.meshes) {
2290
+ // The depth block belongs here more than it belongs in `build`: `explain`
2291
+ // is the command that says what a spec MEANS, and the turn ceiling is the
2292
+ // number an author needs before writing a key rather than after a refusal.
2293
+ // It was absent, while `docs/AUTHORING.md` said both commands printed it.
2222
2294
  console.log(
2223
2295
  ` ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
2224
- `${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}`,
2296
+ `${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}` +
2297
+ meshDepthNote(m),
2225
2298
  );
2226
2299
  }
2227
2300
  }
@@ -2248,12 +2321,13 @@ function cmdExplain(flags: Record<string, string>): void {
2248
2321
  const FLAG_MEANINGS: Record<string, string> = {
2249
2322
  rig: 'the rig spec — skeleton structure',
2250
2323
  motion: 'the motion spec — time',
2251
- out: 'directory for skeleton.json + skeleton.atlas; atlas page paths are written relative to it',
2324
+ out: 'directory for skeleton.json + skeleton.atlas; atlas page paths and skeleton.images are written relative to it',
2252
2325
  images: "override the rig spec's own images directory (relative to your working directory)",
2253
2326
  manifest: 'a cut manifest, for a rig with measured art behind it; a foreign skeleton has none',
2254
2327
  'copy-images':
2255
2328
  'also copy every referenced page PNG into --out and rewrite the atlas to the copies, so the directory is ' +
2256
- 'self-contained enough to zip or commit on its own (default: page paths still point at the source art)',
2329
+ 'self-contained enough to zip or commit on its own, and point skeleton.images at --out itself so the editor finds ' +
2330
+ 'the parts beside the skeleton on import (default: page paths still point at the source art)',
2257
2331
  pack: 'arrange every part PNG onto shared atlas page(s) written into --out as real PNGs, instead of one page ' +
2258
2332
  'per part. Lossless: every region is a byte-for-byte copy and nothing is resampled, trimmed or rotated ' +
2259
2333
  '(default: one part, one page, pointing at the source art)',