spine-rigc 1.9.3 → 2.0.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/.claude-plugin/plugin.json +1 -1
- package/NOTICE.md +37 -21
- package/README.md +38 -20
- package/bin/rigc.cjs +45 -2
- package/cli_core.ts +35 -0
- package/docs/AUTHORING.md +27 -15
- package/package.json +4 -5
- package/skills/rigc/SKILL.md +8 -5
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +53 -3
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +44 -2
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/emitted/index.ts +119 -0
- package/src/assertions/report.ts +64 -0
- package/src/cli/core_commands.ts +82 -7
- package/src/cli/shared.ts +567 -59
- package/src/cli/spine_commands.ts +36 -366
- package/src/validate.ts +16 -86
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "rigc",
|
|
4
|
-
"description": "Author Spine 4.3 rigs with rigc, the rig compiler that verifies its own output
|
|
4
|
+
"description": "Author Spine 4.3 rigs with rigc, the rig compiler that verifies its own output with named assertions before writing it. 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
5
|
"author": {
|
|
6
6
|
"name": "firejune",
|
|
7
7
|
"url": "https://github.com/firejune"
|
package/NOTICE.md
CHANGED
|
@@ -2,16 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Spine Runtimes
|
|
4
4
|
|
|
5
|
-
This
|
|
5
|
+
This repository depends on `@esotericsoftware/spine-core`, part of the
|
|
6
6
|
[Spine Runtimes](https://github.com/EsotericSoftware/spine-runtimes),
|
|
7
7
|
Copyright (c) 2013-2025 Esoteric Software LLC, licensed under the
|
|
8
|
-
[Spine Runtimes License Agreement](https://esotericsoftware.com/spine-runtimes-license)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
the
|
|
8
|
+
[Spine Runtimes License Agreement](https://esotericsoftware.com/spine-runtimes-license),
|
|
9
|
+
as a **development dependency**: a clone and CI install it, and every Spine file
|
|
10
|
+
`rigc build` writes there is read back through it before it is written. **The
|
|
11
|
+
published package does not carry it** — an install of `spine-rigc` has no Spine
|
|
12
|
+
runtime in it unless one is installed beside it, and then the same `rigc` uses it.
|
|
13
|
+
|
|
14
|
+
Its terms, as the licence states them: integration of the Spine Runtimes into
|
|
15
|
+
software is permitted **under the terms and conditions of Section 2 of the
|
|
16
|
+
[Spine Editor License Agreement](https://esotericsoftware.com/spine-editor-license)**;
|
|
17
|
+
otherwise it is permitted **provided that each user of the Products obtains their
|
|
18
|
+
own Spine Editor license** and any redistribution of the Products includes the
|
|
19
|
+
licence and copyright notice. Where a Spine Runtime is integrated with rigc — this
|
|
20
|
+
repository, its CI, or an install with the runtime beside it — those terms apply
|
|
21
|
+
there.
|
|
15
22
|
|
|
16
23
|
### What that means for rigc, as a chain of facts
|
|
17
24
|
|
|
@@ -19,16 +26,26 @@ rigc's own code is MIT (see `LICENSE`). The following is a restatement of Esoter
|
|
|
19
26
|
Software's terms, not a licence term of this project:
|
|
20
27
|
|
|
21
28
|
1. rigc's output **is Spine skeleton data**.
|
|
22
|
-
2.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
2. A product that plays it with **a Spine Runtime** — the official runtimes, the Web
|
|
30
|
+
Player — has integrated the Spine Runtimes. rigc also writes the model document
|
|
31
|
+
its own core poses, and a product may play that or anything else; what the terms
|
|
32
|
+
turn on is the integration.
|
|
33
|
+
3. Integrating a Spine Runtime into a product is permitted **under Section 2 of the
|
|
34
|
+
Spine Editor License Agreement**, or otherwise on the condition that **each user
|
|
35
|
+
of the product obtains their own Spine Editor license** and the product carries
|
|
36
|
+
the Runtimes licence and copyright notice — the two routes above.
|
|
37
|
+
4. The published rigc **links no Spine runtime**. This repository does, as a
|
|
38
|
+
development dependency for its own gate, and so does an install with
|
|
39
|
+
`spine-core` added beside it. Validation is not optional on either: the gate
|
|
40
|
+
runs through the runtime where it is installed and through rigc's own validator
|
|
41
|
+
where it is not, and the two are held to the same verdicts in CI on 48 of the
|
|
42
|
+
49 assertions — the official parser's own parse runs only where the runtime is.
|
|
43
|
+
|
|
44
|
+
> **If a product integrates a Spine Runtime to play rigc's output, that integration
|
|
45
|
+
> is subject to the terms above** — rigc neither creates those terms nor removes
|
|
46
|
+
> them. Running the published rigc links no Spine runtime, and whether its output is
|
|
47
|
+
> then played by one, by rigc's own core, or by something else is the consumer's
|
|
48
|
+
> choice.
|
|
32
49
|
|
|
33
50
|
### The Spine Web Player, and what `rigc preview` does with it
|
|
34
51
|
|
|
@@ -47,9 +64,8 @@ Two consequences worth stating plainly:
|
|
|
47
64
|
|
|
48
65
|
- a generated preview needs a network connection the first time it is opened, and
|
|
49
66
|
says so in the page when the player does not arrive;
|
|
50
|
-
- the
|
|
51
|
-
|
|
52
|
-
of a product built this way needs their own Spine Editor license.**
|
|
67
|
+
- using the Spine Web Player is a Spine Runtimes integration, subject to the
|
|
68
|
+
terms described above.
|
|
53
69
|
|
|
54
70
|
## Example assets
|
|
55
71
|
|
package/README.md
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
**AI-authored Spine 2D rigging and animation, verified before it is written.** rigc is
|
|
12
12
|
a rig compiler for Spine: a rig spec and a motion spec in, Spine 4.3 skeleton data out,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
gated by a list of named assertions before a byte is written — rigc's own validator in
|
|
14
|
+
the published package, held to a `spine-core` round trip's verdicts in this
|
|
15
|
+
repository's CI. Built so AI agents can author rigs and check their own work; it ships
|
|
16
|
+
as an agent skill.
|
|
16
17
|
|
|
17
18
|
## What you get
|
|
18
19
|
|
|
@@ -26,8 +27,10 @@ the breathing and the wave are all rigc-compiled Spine animations, rendered with
|
|
|
26
27
|
|
|
27
28
|
Loose part PNGs and two small JSON files in; **Spine 4.3 skeleton data out** — a
|
|
28
29
|
`skeleton.json` and a `skeleton.atlas` that load in any Spine runtime and **import
|
|
29
|
-
into the Spine editor**. Nothing is written unless a
|
|
30
|
-
|
|
30
|
+
into the Spine editor**. Nothing is written unless a list of named assertions comes
|
|
31
|
+
back green — assertions this repository holds to the verdicts of a round-trip through
|
|
32
|
+
Spine's own parser, all but the parse itself, which runs only where the runtime is
|
|
33
|
+
installed.
|
|
31
34
|
|
|
32
35
|
| You have | You run | You get |
|
|
33
36
|
| --- | --- | --- |
|
|
@@ -53,10 +56,16 @@ it cuts both ways:
|
|
|
53
56
|
drafts, a human refines in the editor**. That hand-off is what emitting somebody
|
|
54
57
|
else's format buys, and `tools/editor_roundtrip.ts` measures that it survives
|
|
55
58
|
the trip in both directions.
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
- The published package **links no Spine runtime**. `@esotericsoftware/spine-core`
|
|
60
|
+
is a development dependency: this repository uses it to hold rigc's own validator
|
|
61
|
+
to the round trip's verdicts — in CI, on the public example recipes and every
|
|
62
|
+
selftest call, and on a private corpus of production rigs measured before each
|
|
63
|
+
release. Of the 49 assertions, 48 run on both; `A00_ROUNDTRIP_PARSE`, the official
|
|
64
|
+
parser's own parse, runs only where the runtime is installed and is reported as a
|
|
65
|
+
SKIP elsewhere. Install it beside rigc (`bun add -d @esotericsoftware/spine-core`)
|
|
66
|
+
and the same `rigc` runs the round trip on every build. The
|
|
67
|
+
[Spine Runtimes License Agreement](https://esotericsoftware.com/spine-runtimes-license)
|
|
68
|
+
applies to that dependency wherever it is installed.
|
|
60
69
|
|
|
61
70
|
### Licensing, stated plainly
|
|
62
71
|
|
|
@@ -64,15 +73,23 @@ rigc's own code is MIT (see [LICENSE](LICENSE)). That says nothing about Spine,
|
|
|
64
73
|
the following is a restatement of Esoteric Software's terms, not a term of ours:
|
|
65
74
|
|
|
66
75
|
1. rigc's output **is Spine skeleton data**.
|
|
67
|
-
2.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
2. A product that plays it with **a Spine Runtime** — the official runtimes, the Web
|
|
77
|
+
Player — has integrated the Spine Runtimes. rigc also writes the model document
|
|
78
|
+
its own core poses, and a product may play that or anything else; what the terms
|
|
79
|
+
turn on is the integration.
|
|
80
|
+
3. Integrating a Spine Runtime into a product is permitted **under Section 2 of the
|
|
81
|
+
[Spine Editor License Agreement](https://esotericsoftware.com/spine-editor-license)**,
|
|
82
|
+
or otherwise on the condition that **each user of the product obtains their own
|
|
83
|
+
Spine editor licence** and the product carries the Runtimes licence and copyright
|
|
84
|
+
notice — the Runtimes licence's own two routes, in its words.
|
|
85
|
+
4. The published rigc **links no Spine runtime**. This repository does — `spine-core`
|
|
86
|
+
as a development dependency, for its own gate — and so does an install with the
|
|
87
|
+
runtime added beside it.
|
|
88
|
+
|
|
89
|
+
> **If a product integrates a Spine Runtime to play rigc's output, that integration
|
|
90
|
+
> is subject to the terms above** — rigc neither adds those terms nor removes them.
|
|
91
|
+
> Running the published rigc links no Spine runtime, and whether its output is then
|
|
92
|
+
> played by one, by rigc's own core, or by something else is the consumer's choice.
|
|
76
93
|
|
|
77
94
|
See [NOTICE.md](NOTICE.md) for the full notice.
|
|
78
95
|
|
|
@@ -98,8 +115,9 @@ without a murmur — a constraint in the 4.2 shape simply vanishes, a `size:` th
|
|
|
98
115
|
disagrees with the PNG collapses every UV, a four-number curve array yields NaN,
|
|
99
116
|
a mesh whose vertex count happens to equal its UV count silently loses its bone
|
|
100
117
|
weights. Every one of those loads clean, plays, and is wrong. rigc's answer is to
|
|
101
|
-
make the failure legible: compile from a spec,
|
|
102
|
-
|
|
118
|
+
make the failure legible: compile from a spec, run a list of named assertions — held
|
|
119
|
+
to the real parser's verdicts in CI — and **write nothing unless all of them are
|
|
120
|
+
green.**
|
|
103
121
|
|
|
104
122
|
## Install
|
|
105
123
|
|
package/bin/rigc.cjs
CHANGED
|
@@ -13,13 +13,56 @@
|
|
|
13
13
|
* code and signals all pass straight through. If it isn't, say so once and
|
|
14
14
|
* stop. No downloads, no network, no writes — just the hand-off or the
|
|
15
15
|
* message.
|
|
16
|
+
*
|
|
17
|
+
* ## Which entry (issue #1061)
|
|
18
|
+
*
|
|
19
|
+
* `@esotericsoftware/spine-core` is a devDependency: a clone and CI have it, an
|
|
20
|
+
* install of the package does not unless somebody adds it beside. So the
|
|
21
|
+
* hand-off has two targets and one rule. When the runtime resolves from this
|
|
22
|
+
* package's own location — Node's resolution, from the package root, which is
|
|
23
|
+
* what an import in `cli.ts` would find — the target is `cli.ts`, every
|
|
24
|
+
* command, the round trip included. When it does not, the target is
|
|
25
|
+
* `cli_core.ts`, the entry that links nothing of the runtime and refuses by
|
|
26
|
+
* name the commands that need it. Nothing else chooses: no environment
|
|
27
|
+
* variable, no flag. `--version` says which ran and why, on stderr, so the
|
|
28
|
+
* version on stdout stays one line a script can read.
|
|
16
29
|
*/
|
|
17
30
|
|
|
18
31
|
const path = require('node:path');
|
|
32
|
+
const fs = require('node:fs');
|
|
19
33
|
const { spawnSync } = require('node:child_process');
|
|
20
34
|
|
|
21
|
-
const
|
|
22
|
-
const
|
|
35
|
+
const root = path.join(__dirname, '..');
|
|
36
|
+
const RUNTIME = '@esotericsoftware/spine-core';
|
|
37
|
+
|
|
38
|
+
/** The runtime's version where it resolves from the package root, or null where it does not. */
|
|
39
|
+
function runtimeVersion() {
|
|
40
|
+
let manifest;
|
|
41
|
+
try {
|
|
42
|
+
manifest = require.resolve(`${RUNTIME}/package.json`, { paths: [root] });
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const version = JSON.parse(fs.readFileSync(manifest, 'utf8')).version;
|
|
48
|
+
return typeof version === 'string' ? version : '(no version field)';
|
|
49
|
+
} catch {
|
|
50
|
+
return '(unreadable package.json)';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const present = runtimeVersion();
|
|
55
|
+
const entry = present === null ? 'cli_core.ts' : 'cli.ts';
|
|
56
|
+
const args = process.argv.slice(2);
|
|
57
|
+
const result = spawnSync('bun', [path.join(root, entry), ...args], { stdio: 'inherit' });
|
|
58
|
+
|
|
59
|
+
if (!result.error && result.status === 0 && args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
|
|
60
|
+
process.stderr.write(
|
|
61
|
+
present === null
|
|
62
|
+
? `entry: ${entry} — ${RUNTIME} absent — the round trip and the commands that need it are not available here; see --help\n`
|
|
63
|
+
: `entry: ${entry} — ${RUNTIME} ${present} present\n`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
23
66
|
|
|
24
67
|
if (result.error) {
|
|
25
68
|
if (result.error.code === 'ENOENT') {
|
package/cli_core.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* rigc's second entry: the commands that reach nothing of spine-core (issue
|
|
4
|
+
* #1052, step 4e of #380).
|
|
5
|
+
*
|
|
6
|
+
* bun cli_core.ts render --candidate <dir> [flags]
|
|
7
|
+
* bun cli_core.ts check --candidate <dir> --frames <dir> [flags]
|
|
8
|
+
* bun cli_core.ts --help the commands this entry runs
|
|
9
|
+
*
|
|
10
|
+
* Which commands those are is not listed here: it is every command whose
|
|
11
|
+
* `runtime` is `false` in the one command table (`src/cli/shared.ts`), and
|
|
12
|
+
* `--help` prints exactly that set. This file imports nothing that links the
|
|
13
|
+
* runtime — no module it reaches, statically, imports
|
|
14
|
+
* `@esotericsoftware/spine-core` — so it runs with the package absent
|
|
15
|
+
* (`RC24`/`RC25` in `selftest.ts` build that state and hold it). Each command
|
|
16
|
+
* prints and writes what `cli.ts` prints and writes for it.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ What it cannot do, and refuses by name rather than reaching for: an input
|
|
19
|
+
* only the runtime reads — a Spine export, `--poser spine`, a fallback the
|
|
20
|
+
* poser line names, the deform survey of a build the core refuses — and a
|
|
21
|
+
* command whose body is the runtime's (`validate`, `bench`, `bonedist`,
|
|
22
|
+
* `preview`, `vote`). Both say what they needed spine-core for and that this
|
|
23
|
+
* entry links none of it; `cli.ts` runs them.
|
|
24
|
+
*
|
|
25
|
+
* `build` is the one name with a body here of its own (issue #1060,
|
|
26
|
+
* `CORE_ENTRY_RUNS`): `cli.ts build`'s body (`runBuild`), writing the same
|
|
27
|
+
* files, with another gate — the model side over the document and the round
|
|
28
|
+
* trip's own rules restated over the emitted text, A00 (spine-core's parse) a
|
|
29
|
+
* SKIP naming it. `--help` and `build --help` say so; the command table states
|
|
30
|
+
* it (`runtime.core` in `src/cli/shared.ts`).
|
|
31
|
+
*/
|
|
32
|
+
import { CORE_COMMAND_RUNS, CORE_ENTRY_RUNS } from './src/cli/core_commands.ts';
|
|
33
|
+
import { runCli } from './src/cli/shared.ts';
|
|
34
|
+
|
|
35
|
+
runCli({ checkout: 'cli_core.ts', linksRuntime: false, runs: { ...CORE_COMMAND_RUNS, ...CORE_ENTRY_RUNS } }, process.argv.slice(2));
|
package/docs/AUTHORING.md
CHANGED
|
@@ -132,11 +132,23 @@ bun cli.ts vote --candidate path/to/spine-a --candidate path/to/spine-b # -> b
|
|
|
132
132
|
bun cli.ts vote --record vote-<id>.json # -> votes.jsonl
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
`build` compiles,
|
|
136
|
-
|
|
135
|
+
`build` compiles, runs the named assertions — through a `@esotericsoftware/spine-core`
|
|
136
|
+
round trip where the runtime is installed, through rigc's own validator where it is
|
|
137
|
+
not (§5.2 says which rules run on each) — and **writes only if every one of them is
|
|
138
|
+
green.** A red
|
|
137
139
|
run leaves nothing on disk, so there is no half-written artifact to mistake for a
|
|
138
140
|
result. There is no `--no-validate`, and there will not be one.
|
|
139
141
|
|
|
142
|
+
`bun cli_core.ts build` — the entry that links none of spine-core — takes the same
|
|
143
|
+
flags and writes the same files, byte for byte: `skeleton.json`, `skeleton.atlas`,
|
|
144
|
+
`skeleton.model.json`, and the pages `--pack` or `--copy-images` put in `--out`.
|
|
145
|
+
Only the gate differs, because the round trip is spine-core's: it runs the model
|
|
146
|
+
side over the document, and the round trip's own rules restated over the text the
|
|
147
|
+
emitter wrote (A01, A02, A05, A07, A16, A31, A35, and A18 over a second compile's
|
|
148
|
+
three files), and it does not run `A00_ROUNDTRIP_PARSE`, which reports **SKIP**
|
|
149
|
+
naming spine-core. Its report ends on a line saying which rules ran there and
|
|
150
|
+
which did not, and it writes only when none of them fails (§5.2).
|
|
151
|
+
|
|
140
152
|
`check` is the second half, and it is only skippable if nobody gave you pictures.
|
|
141
153
|
Green from `build` means the file is valid; it says nothing at all about whether
|
|
142
154
|
the animation is the one in the frames, and there is no assertion that could — see
|
|
@@ -207,7 +219,7 @@ What the flags mean:
|
|
|
207
219
|
| `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
|
|
208
220
|
| `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
|
|
209
221
|
| `--profile` | `spine` = the 34 validity rules (**the default**) · `spine-html` = all 49, opt-in |
|
|
210
|
-
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
222
|
+
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. A directory holding no `skeleton.json`, or a path with nothing at it, is refused `nothing at <path>`, exit 2, nothing written, by every command that takes a build — `validate <dir>` and a directory given to `diff` included (`RC27`). `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
211
223
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
|
|
212
224
|
| `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
|
|
213
225
|
| `--poser` | `render`, `check` and `explain`: `core` or `spine` — on `render` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`). On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1) |
|
|
@@ -5841,14 +5853,14 @@ Fix A00 and run it again.
|
|
|
5841
5853
|
|
|
5842
5854
|
| Assertion | Profile | What tripped it, and where to fix it |
|
|
5843
5855
|
| --- | --- | --- |
|
|
5844
|
-
| `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Almost everything else in the report is downstream of this one — fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked. ⚠️ **Two rules run before it and can be upstream of it**: `A31_DRAW_ORDER_OFFSETS_RESOLVE`, because a bad draw-order key makes the loader spin rather than return, and `A08_REGION_NAMES_MATCH_ATTACHMENTS`, because a `path` naming no region makes it throw. The round trip is still attempted either way; when the loader refuses a path A08 has already refused, this row **defers** to A08 by name instead of restating the miss in the parser's poorer words |
|
|
5845
|
-
| `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS` | both | a 4.1/4.2-shaped `ik`/`transform`/`path`/`physics`/`slider` array. rigc emits the 4.3 `constraints` array, so this normally means hand-edited JSON |
|
|
5846
|
-
| `A02_NO_BONE_TRANSFORM_KEY` | both | `bone "…" uses "transform", the key 4.0 and 4.1 spelled; 4.2 and 4.3 spell it "inherit"` — rename it `inherit` in the rig spec |
|
|
5856
|
+
| `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Almost everything else in the report is downstream of this one — fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked. ⚠️ **Two rules run before it and can be upstream of it**: `A31_DRAW_ORDER_OFFSETS_RESOLVE`, because a bad draw-order key makes the loader spin rather than return, and `A08_REGION_NAMES_MATCH_ATTACHMENTS`, because a `path` naming no region makes it throw. The round trip is still attempted either way; when the loader refuses a path A08 has already refused, this row **defers** to A08 by name instead of restating the miss in the parser's poorer words Under `cli_core.ts build`, which links none of spine-core, it is a **SKIP** naming spine-core — the one rule of the 49 that entry does not run; `bun cli.ts build` and `rigc validate` run it (`RC28`). |
|
|
5857
|
+
| `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS` | both | a 4.1/4.2-shaped `ik`/`transform`/`path`/`physics`/`slider` array. rigc emits the 4.3 `constraints` array, so this normally means hand-edited JSON Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5858
|
+
| `A02_NO_BONE_TRANSFORM_KEY` | both | `bone "…" uses "transform", the key 4.0 and 4.1 spelled; 4.2 and 4.3 spell it "inherit"` — rename it `inherit` in the rig spec Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5847
5859
|
| `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height`. **SKIP** when the skeleton carries no region attachment |
|
|
5848
5860
|
| `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment |
|
|
5849
|
-
| `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel**. **SKIP** when no animation carries a timeline at all. Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one |
|
|
5850
|
-
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk — the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and §0.2 has the whole of it: a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles: a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels — a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling**, and what that message names is the pair that shares texels. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes — one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way — or calls it **truncated** when it begins as a PNG and runs out before its `IEND` (§0.2). That clause is validity too: nothing in rigc can read such a page back. **SKIP** when the atlas declares no page |
|
|
5851
|
-
| `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` |
|
|
5861
|
+
| `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel**. **SKIP** when no animation carries a timeline at all. Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5862
|
+
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk — the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and §0.2 has the whole of it: a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles: a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels — a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling**, and what that message names is the pair that shares texels. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes — one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way — or calls it **truncated** when it begins as a PNG and runs out before its `IEND` (§0.2). That clause is validity too: nothing in rigc can read such a page back. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page's file is **not on disk** and nothing else here failed: that page's size was not measured, so the reason names the pages not read (or says none was, when every file is missing) and points at `A17`, which fails naming each missing file — one missing file is named once, by the rule whose subject it is. With another `A06` failure standing, the missing page is `A17`'s alone |
|
|
5863
|
+
| `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5852
5864
|
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. There is no `spine-html` clause here — a placeholder is free to differ from the region its `path` names **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once |
|
|
5853
5865
|
| `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
|
|
5854
5866
|
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked. 🧍 **A skeleton with no animation is still read**: its setup pose is the whole of what it shows, so A10 reads it — the world transform and the vertices as above, and the bone's mode and the slot colours a stepped frame is read for, which on a static rig nothing else would reach — and names it `the setup pose` (`the setup pose: slot "x" colour is non-finite`). The stepping half had nothing, and a PASS row carries no detail, so the stats line says so: `nanStepping=skipped`, beside `animations=0`. `A09` does not follow — a declared duration is a fact about an animation and nothing else, so a static rig still gives it nothing. **SKIP** only when the skeleton carries **no bone and no animation**: there is then no pose to read and nothing to step |
|
|
@@ -5857,10 +5869,10 @@ Fix A00 and run it again.
|
|
|
5857
5869
|
| `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
|
|
5858
5870
|
| `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip. **SKIP** when the skeleton declares no stage (§3.1): there is no full frame to span, and *unmeasured* must not print the same green as *measured and clear* |
|
|
5859
5871
|
| `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. The rule assumes meshes are mostly static — the renderer skips redrawing a mesh nothing moved — so it has three states. **FAIL per bone** when the rig declares nothing: `idle keys bone "X", which drives a mesh — meshes never idle-skip`, and the **first** of those lines, once per run, names the case: `K bone(s) keyed by idle drive meshes; if this idle is meant to deform them (a painting rig), declare invariants.idleDrivesMeshes: { "why": … } in the rig spec — or, where the motion belongs to a pivot above the mesh, key that pivot one link up (FACE.md §3)`. It rides the first finding, so the count of FAIL lines is still the count of bones. **SKIP with the cost** when the rig declares `invariants.idleDrivesMeshes` (§3.7): `declared by the rig ("<why>"): idle keys N bone(s) that drive M mesh attachment(s) totalling V vertices — "a", "b" — and each of those meshes is recomputed on every frame it is shown`. **FAIL on a stale declaration** — declared, and the `idle` keys no mesh-driving bone, carries no bone timeline or does not exist: `the rig "X" declares invariants.idleDrivesMeshes ("…"), but "idle" keys no bone that drives a mesh, so the declaration switches off nothing — remove it`. Otherwise **SKIP** when there is no `idle` animation, or when the one there is carries no bone timeline — a rule whose subject does not exist is unmeasured and not satisfied |
|
|
5860
|
-
| `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
|
|
5861
|
-
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. **SKIP** when the atlas declares no page — as it is for `A06`, `A19` and `A27`; see `A07` |
|
|
5862
|
-
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed — in `skeleton.json`, `skeleton.atlas` or the model document `skeleton.model.json`, the one named (the document's first differing line with it). That is a compiler bug, not a spec bug — report it |
|
|
5863
|
-
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes**; the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes: the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art. ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page**: a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it**, and the clause above does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so a scan would come back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappears**, the scan finding a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. ⚠️ **A page file that cannot be read as PNG at all is the same non-measurement for every part on it**: one row per page naming its parts and pointing at `A06`, which names what the file is. **SKIP** when the atlas declares no page |
|
|
5872
|
+
| `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5873
|
+
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. This is the one rule that names a missing page file; `A06` and `A19` SKIP over the page they could not read and point here rather than naming it again. **SKIP** when the atlas declares no page — as it is for `A06`, `A19` and `A27`; see `A07` |
|
|
5874
|
+
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed — in `skeleton.json`, `skeleton.atlas` or the model document `skeleton.model.json`, the one named (the document's first differing line with it). That is a compiler bug, not a spec bug — report it Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5875
|
+
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes**; the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes: the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art. ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page**: a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it**, and the clause above does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so a scan would come back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappears**, the scan finding a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. ⚠️ **A page file that cannot be read as PNG at all is the same non-measurement for every part on it**: one row per page naming its parts and pointing at `A06`, which names what the file is. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page carrying a part has no file on disk and nothing else here failed: that page's parts were not read, so the reason names the pages not read and how many of the pages carrying a part were (or says none was) and points at `A17`, which fails naming each missing file. A missing page holding only the base plate moves nothing here: this rule had no part on it to read |
|
|
5864
5876
|
| `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. On a rig spec's `"boneIndexing": "raw"` mesh an index out of range or a run that does not decode is refused at compile first (§5.1, issue #917), so here it names a skeleton rigc did not compile. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not. Fix the rig spec's `controls`/`chain`, a `segments` generator's `bones` (its `influence` line names a bone no vertex binds), or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
|
|
5865
5877
|
| `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1. **SKIPs** by name on authored or linked geometry and on a `segments` mesh, whose outline is weighted by distance like the rest of it — none of them claims a pinned rim |
|
|
5866
5878
|
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment |
|
|
@@ -5872,11 +5884,11 @@ Fix A00 and run it again.
|
|
|
5872
5884
|
| `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width. **SKIPs** on authored geometry, a contour mesh and a `segments` lattice — none has rows rigc paired |
|
|
5873
5885
|
| `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
|
|
5874
5886
|
| `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
|
|
5875
|
-
| `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted |
|
|
5887
|
+
| `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5876
5888
|
| `A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
|
|
5877
5889
|
| `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index (on a rig spec's raw run rigc refuses both at compile first — §5.1, issue #917), a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
|
|
5878
5890
|
| `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path`, `physics` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.4, §4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. The **empty** name under `physics` is not a miss — it is the timeline that names no constraint (§4.4's `"*"`), and it is refused only when it reaches none: `animation "A" physics constraint "" timeline "P": a physics group that names no constraint writes every physics constraint declaring "PGlobal": true, and none of "C", … does — the parser loads it, the runtime walks every constraint, and no constraint takes the key`. Who it reaches is asked of the runtime's own `PhysicsConstraintTimeline.global` on the file's constraints — the reading `A23` and `A42` share. **SKIP** when no animation carries one |
|
|
5879
|
-
| `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11). **SKIP** when no animation carries a deform timeline |
|
|
5891
|
+
| `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11). **SKIP** when no animation carries a deform timeline Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5880
5892
|
| `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying any of them **above 0** (§3.5.1, §4.12). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. The third reads the values the loaded timeline poses — keys and Bezier samples alike — the way `A23` reads a physics `mix`, so a `mix` timeline keying 0 only is not a rescue: [measured] it poses every bone exactly where no timeline does. The detail says both halves: `path constraint "P" has mixRotate 0, mixX 0 and mixY 0 at setup and none of the 2 animations keys its mix above 0; update() returns on all-zero mixes, so nothing ever puts a bone on the path — rest one of the three above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no path constraint |
|
|
5881
5893
|
| `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` **above 0** (§3.5.2, §4.12) — the same reading as `A36`'s, so a `mix` timeline keying 0 only is no rescue: `slider "S" has mix 0 at setup and none of the 2 animations keys its mix above 0; update() returns on mix 0 — rest it above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no slider |
|
|
5882
5894
|
| `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data,
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"rigc": "./bin/rigc.cjs"
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"files": [
|
|
56
56
|
"bin/rigc.cjs",
|
|
57
57
|
"cli.ts",
|
|
58
|
+
"cli_core.ts",
|
|
58
59
|
"src",
|
|
59
60
|
"tools/plate.ts",
|
|
60
61
|
"tools/font5x7.ts",
|
|
@@ -90,10 +91,8 @@
|
|
|
90
91
|
"viewer": "vite --config viewer/vite.config.ts",
|
|
91
92
|
"prepublishOnly": "bun run typecheck && bun run lint && bun run selftest"
|
|
92
93
|
},
|
|
93
|
-
"dependencies": {
|
|
94
|
-
"@esotericsoftware/spine-core": "4.3.13"
|
|
95
|
-
},
|
|
96
94
|
"devDependencies": {
|
|
95
|
+
"@esotericsoftware/spine-core": "4.3.13",
|
|
97
96
|
"@types/bun": "^1.4.0",
|
|
98
97
|
"eslint": "^10.9.0",
|
|
99
98
|
"spine-html": "^0.4.1",
|
package/skills/rigc/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rigc
|
|
3
|
-
description: Author, build and validate Spine 4.3 skeleton data (skeleton.json plus its .atlas) from loose part PNGs with rigc, the rig compiler that verifies its own output
|
|
3
|
+
description: Author, build and validate Spine 4.3 skeleton data (skeleton.json plus its .atlas) from loose part PNGs with rigc, the rig compiler that verifies its own output with named assertions before writing it. Use for any request to make a Spine rig or Spine animation from PNG parts, or where the source is a Live2D, Unity or video model whose pictures you can render, to run or read rigc build, validate, render, preview, check or vote, or to write or fix a *.rig.json or *.motion.json spec; it says which shipped guide to open for the need at hand. Not for Live2D conversion, cutting an illustration into parts, or real-time face tracking.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Requires Bun 1.2 or later. The tool is the npm package spine-rigc (bunx spine-rigc, or bun add -d spine-rigc); the command it installs is rigc.
|
|
6
6
|
---
|
|
@@ -8,8 +8,9 @@ compatibility: Requires Bun 1.2 or later. The tool is the npm package spine-rigc
|
|
|
8
8
|
# rigc — a rig compiler for agents
|
|
9
9
|
|
|
10
10
|
rigc compiles a rig spec and a motion spec into Spine 4.3 skeleton data and an
|
|
11
|
-
atlas,
|
|
12
|
-
|
|
11
|
+
atlas, runs its named assertions — rigc's own validator in the published package,
|
|
12
|
+
held to a `@esotericsoftware/spine-core` round trip's verdicts in its CI, all but
|
|
13
|
+
the parse itself — and writes **only if every one is green**. You cannot see the
|
|
13
14
|
rig you are authoring. The validator's messages and the shipped guides are the
|
|
14
15
|
whole interface, and this skill only says which of them to open.
|
|
15
16
|
|
|
@@ -17,8 +18,10 @@ whole interface, and this skill only says which of them to open.
|
|
|
17
18
|
|
|
18
19
|
- **Validation is never bypassed.** `build` writes nothing on a red gate; there is
|
|
19
20
|
no `--no-validate`, and none may be added — AUTHORING §0 says so in as many
|
|
20
|
-
words. Correctness is the whole of the reason: the
|
|
21
|
-
|
|
21
|
+
words. Correctness is the whole of the reason: the assertions are what make the
|
|
22
|
+
output trustworthy, and they are held to the official parser's verdicts in the
|
|
23
|
+
repository's CI — all but the parse itself, which runs only where the runtime is
|
|
24
|
+
installed and is reported as a SKIP elsewhere.
|
|
22
25
|
- **The compiler never invents a value.** A field the spec leaves out is a
|
|
23
26
|
`CompileError` naming that field. Fill the spec; do not expect a default read off
|
|
24
27
|
the art — AUTHORING §2 and §5.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A01, restated over the emitted text (issue #1060): the clause
|
|
3
|
+
* `validate()` runs in `src/validate.ts`, word for word, over the skeleton JSON — the
|
|
4
|
+
* text rigc wrote, read by rigc's own reader rather than by spine-core's. The
|
|
5
|
+
* round trip's copy is unchanged and runs in `cli.ts build`; this one runs in
|
|
6
|
+
* the gate of the entry that links none of the runtime
|
|
7
|
+
* (`../emitted/index.ts`), and the selftest's `RC28` holds the two to the
|
|
8
|
+
* same lines on every recipe and on this assertion's mutants.
|
|
9
|
+
*/
|
|
10
|
+
import type { Verdicts } from '../harness.ts';
|
|
11
|
+
import { TOPLEVEL_CONSTRAINT_ARRAYS } from '../../generation.ts';
|
|
12
|
+
import type { Json } from '../values.ts';
|
|
13
|
+
|
|
14
|
+
export function a01NoLegacyToplevelConstraintArrays({ fail }: Verdicts, raw: Json | null): void {
|
|
15
|
+
for (const key of TOPLEVEL_CONSTRAINT_ARRAYS) {
|
|
16
|
+
if (raw && key in raw) {
|
|
17
|
+
fail(
|
|
18
|
+
'A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS',
|
|
19
|
+
`top-level "${key}" array present; 4.3 wants it inside "constraints" with type:"${key}"`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A02, restated over the emitted text (issue #1060): the clause
|
|
3
|
+
* `validate()` runs in `src/validate.ts`, word for word, over the skeleton JSON — the
|
|
4
|
+
* text rigc wrote, read by rigc's own reader rather than by spine-core's. The
|
|
5
|
+
* round trip's copy is unchanged and runs in `cli.ts build`; this one runs in
|
|
6
|
+
* the gate of the entry that links none of the runtime
|
|
7
|
+
* (`../emitted/index.ts`), and the selftest's `RC28` holds the two to the
|
|
8
|
+
* same lines on every recipe and on this assertion's mutants.
|
|
9
|
+
*/
|
|
10
|
+
import type { Verdicts } from '../harness.ts';
|
|
11
|
+
import { LEGACY_BONE_INHERIT_KEY } from '../../generation.ts';
|
|
12
|
+
import { isObj, type Json } from '../values.ts';
|
|
13
|
+
|
|
14
|
+
export function a02NoBoneTransformKey({ fail }: Verdicts, raw: Json | null): void {
|
|
15
|
+
const bones = Array.isArray(raw?.bones) ? (raw.bones as unknown[]) : [];
|
|
16
|
+
for (const bone of bones) {
|
|
17
|
+
if (isObj(bone) && LEGACY_BONE_INHERIT_KEY in bone) {
|
|
18
|
+
fail('A02_NO_BONE_TRANSFORM_KEY', `bone "${String(bone.name)}" uses "transform", the key 4.0 and 4.1 spelled; 4.2 and 4.3 spell it "inherit"`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|