spine-rigc 0.25.5 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/cli.ts +149 -16
- package/docs/AUTHORING.md +363 -61
- package/docs/INGEST.md +13 -4
- package/docs/SPEC_COVERAGE.md +8 -6
- package/package.json +1 -1
- package/src/atlas.ts +11 -2
- package/src/check.ts +59 -1
- package/src/compile.ts +513 -107
- package/src/diff.ts +14 -1
- package/src/emit.ts +76 -41
- package/src/ingest.ts +126 -14
- package/src/mesh.ts +40 -0
- package/src/render.ts +69 -7
- package/src/rig.ts +164 -32
- package/src/slots.ts +169 -27
- package/src/types.ts +41 -0
- package/src/validate.ts +439 -46
package/README.md
CHANGED
|
@@ -413,8 +413,11 @@ that does not belong to it. See
|
|
|
413
413
|
against frames of itself — is the floor the rest are read against, because
|
|
414
414
|
`check` grades nothing and has no pass mark. It is the same instrument and the
|
|
415
415
|
same commands; what changes is that the reference is a build of yours you have
|
|
416
|
-
already looked at, so what it measures is **what your edit did**.
|
|
417
|
-
|
|
416
|
+
already looked at, so what it measures is **what your edit did**. Every drift
|
|
417
|
+
the report prints carries the bound its own match gives it on the line under
|
|
418
|
+
it, so a figure is read against that rather than against a number from a page;
|
|
419
|
+
AUTHORING.md §9.2 says what the two halves of that bound are and why the floor
|
|
420
|
+
is not zero.
|
|
418
421
|
- [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) is the benchmark: the same job, from a brief
|
|
419
422
|
and rendered frames, scored. [docs/PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) is how to run an
|
|
420
423
|
agent through it and score what comes back.
|
|
@@ -530,9 +533,9 @@ first three work on any reference you have, and `bench` is a repository workflow
|
|
|
530
533
|
and `bun run fetch-examples`. The reasoning behind them is in
|
|
531
534
|
[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
|
|
532
535
|
|
|
533
|
-
`build` and `validate` both default to `--profile spine` — the
|
|
536
|
+
`build` and `validate` both default to `--profile spine` — the 29 validity rules, which
|
|
534
537
|
ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
|
|
535
|
-
adds all
|
|
538
|
+
adds all 44: the other 15 are one renderer's policy and one canvas budget's, and they
|
|
536
539
|
fire on perfectly correct editor-produced Spine data, which is why they are opt-in.
|
|
537
540
|
⇒ **That reason is about foreign data and does not carry to a rig you are authoring
|
|
538
541
|
yourself: author under `--profile spine-html` and read the extra 15 as findings, and
|
|
@@ -584,7 +587,7 @@ what each is worth.
|
|
|
584
587
|
**What it reads is skeleton JSON and nothing else** — no `.spine` project, no binary
|
|
585
588
|
`.skel`, no atlas, no art. So it never invents, and the things it cannot get out of
|
|
586
589
|
the file are **findings** with codes rather than plausible values: a construct the
|
|
587
|
-
spec format cannot hold (`
|
|
590
|
+
spec format cannot hold (`point`, a `sequence` block, an unknown field
|
|
588
591
|
on a bone, slot or constraint) is a blocker, the command exits non-zero, and both
|
|
589
592
|
specs are still written — a spec plus a list of what is missing from it beats no spec.
|
|
590
593
|
One thing it drops on purpose and says so: a path attachment's `lengths`, which is
|
|
@@ -678,7 +681,7 @@ letting `A17` blame the editor for the harness's own doing.
|
|
|
678
681
|
| 📥 **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
|
|
679
682
|
| 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
|
|
680
683
|
| 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
|
|
681
|
-
| 🎓 **[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
|
|
684
|
+
| 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 44 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
|
|
682
685
|
| 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
|
|
683
686
|
| 🗺️ [ROADMAP.md](https://github.com/firejune/rigc/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 has to mean before the number is claimed — conditions rather than a feature list, because direction here comes from what users hit |
|
|
684
687
|
| 📐 [CLAUDE.md](https://github.com/firejune/rigc/blob/main/CLAUDE.md) | **the doctrine** — why the validator's messages are the product, why nothing reaches disk before green, why no number is ever invented, and what a change has to keep. [CONTRIBUTING.md](https://github.com/firejune/rigc/blob/main/CONTRIBUTING.md) calls it worth ten minutes before a first patch. Repository material |
|
|
@@ -735,7 +738,7 @@ quality."* All six, with their verdicts, are in
|
|
|
735
738
|
[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
|
|
736
739
|
|
|
737
740
|
The whole dossier — the yardstick, `diff` and `check` and what neither of them can
|
|
738
|
-
see, every rung, the run viewer, the
|
|
741
|
+
see, every rung, the run viewer, the 44 assertions and the selftest behind them — is
|
|
739
742
|
[docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
|
|
740
743
|
Live rung status is
|
|
741
744
|
[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
|
package/cli.ts
CHANGED
|
@@ -67,9 +67,9 @@ import {
|
|
|
67
67
|
type DeformSpan,
|
|
68
68
|
} from './src/deformmeasure.ts';
|
|
69
69
|
import { diffLines, diffSkeletons, reportedFigures, sectionFigures, type DiffReport } from './src/diff.ts';
|
|
70
|
-
import { ingest, IngestError, INGEST_GUTTERS, type IngestStage } from './src/ingest.ts';
|
|
71
|
-
import {
|
|
72
|
-
import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, packAtlas } from './src/atlas.ts';
|
|
70
|
+
import { ingest, IngestError, IngestSpecRefused, INGEST_GUTTERS, type IngestFinding, type IngestStage } from './src/ingest.ts';
|
|
71
|
+
import { copyAtlasPages } from './src/emit.ts';
|
|
72
|
+
import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, packAtlas, parseAtlasText } from './src/atlas.ts';
|
|
73
73
|
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
74
74
|
import { KEY_TIME_EPSILON } from './src/timelines.ts';
|
|
75
75
|
import { findRung, RUNG_IDS, type RungSkeleton } from './src/ladder.ts';
|
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
} from './src/render.ts';
|
|
118
118
|
import {
|
|
119
119
|
assertionCountForProfile,
|
|
120
|
+
attachmentRegionJoins,
|
|
120
121
|
CLI_DEFAULT_PROFILE,
|
|
121
122
|
reportLines,
|
|
122
123
|
validate,
|
|
@@ -148,6 +149,14 @@ export type CutTable = Record<string, CutEntry>;
|
|
|
148
149
|
|
|
149
150
|
class UsageError extends Error {}
|
|
150
151
|
|
|
152
|
+
/**
|
|
153
|
+
* `explain` refusing a pair it cannot pose — a usage error in kind, printed
|
|
154
|
+
* without the usage block for `PoseError`'s and `IngestError`'s reason: the
|
|
155
|
+
* message names an attachment, a region and two flags, and reprinting every
|
|
156
|
+
* command's usage under it buries the one line that says what to change.
|
|
157
|
+
*/
|
|
158
|
+
class ExplainError extends Error {}
|
|
159
|
+
|
|
151
160
|
// ---------------------------------------------------------------------------
|
|
152
161
|
// package metadata — the installed version and repository, for `--version`
|
|
153
162
|
// and for naming a remedy `bench` can only give from a repo checkout.
|
|
@@ -1242,14 +1251,21 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1242
1251
|
// correct for a build sitting beside the project it came from and breaks the
|
|
1243
1252
|
// moment the directory is zipped, committed or moved on its own (issue #217).
|
|
1244
1253
|
// Opt-in only: the default stays exactly what it has always been.
|
|
1254
|
+
//
|
|
1255
|
+
// What is copied is what the ATLAS names, not what the image list holds: under
|
|
1256
|
+
// `--atlas-in` the two are different lists, and rebuilding the text from the
|
|
1257
|
+
// second wrote a file the pack never contained — zero bytes for a rig that
|
|
1258
|
+
// declares no parts, one fabricated page per part for a rig that does, both of
|
|
1259
|
+
// them green here because the gate above had already read the compile's own
|
|
1260
|
+
// text (issue #693, `src/emit.ts`).
|
|
1245
1261
|
let atlasText = result.atlasText;
|
|
1246
1262
|
if (flags['copy-images'] !== undefined) {
|
|
1247
|
-
const copied =
|
|
1263
|
+
const copied = copyAtlasPages(atlasText, opts.outDir);
|
|
1248
1264
|
atlasText = copied.atlasText;
|
|
1249
1265
|
console.log(` .. copy-images: ${copied.pages.length} page(s) copied into ${opts.outDir}`);
|
|
1250
1266
|
for (const p of copied.pages) {
|
|
1251
|
-
const note = p.to === basename(p.from) ? '' :
|
|
1252
|
-
console.log(` .. ${p.
|
|
1267
|
+
const note = p.to === basename(p.from) ? '' : ' (renamed — basename collision)';
|
|
1268
|
+
console.log(` .. ${p.to.padEnd(24)} <- ${p.from} (${p.regions} region(s))${note}`);
|
|
1253
1269
|
}
|
|
1254
1270
|
}
|
|
1255
1271
|
|
|
@@ -2434,6 +2450,81 @@ function cmdBoneDist(flags: Record<string, string>): void {
|
|
|
2434
2450
|
if (flags.json !== undefined) writeJson(flags.json, report);
|
|
2435
2451
|
}
|
|
2436
2452
|
|
|
2453
|
+
/**
|
|
2454
|
+
* Refuse a compiled pair whose art `explain` cannot pose through, by name.
|
|
2455
|
+
*
|
|
2456
|
+
* 🚨 `explain` poses the rig — `deformReportLines` loads the emitted pair through
|
|
2457
|
+
* `spine-core` to measure what each deform key did — and a pose resolves EVERY
|
|
2458
|
+
* attachment against the atlas, whether or not anything deforms it. On the specs
|
|
2459
|
+
* `ingest --art none` writes there is nothing to resolve against: the entries
|
|
2460
|
+
* state a size and name no `image`, so the compile atlases nothing, and the load
|
|
2461
|
+
* threw the runtime's own `Region not found in atlas: rear-upper-arm (attachment:
|
|
2462
|
+
* rear-upper-arm)` with a spine-core stack trace under it and exit 1 (measured on
|
|
2463
|
+
* `examples/spineboy/export/spineboy-ess.json`, issue #697). That is the tool
|
|
2464
|
+
* telling an agent about its own internals instead of about the rig, on the one
|
|
2465
|
+
* input `docs/INGEST.md` §2.0 documents as the route through a foreign skeleton.
|
|
2466
|
+
*
|
|
2467
|
+
* ⭐ Both halves of the join are rigc's own readers rather than a second opinion
|
|
2468
|
+
* on somebody else's format: the walk is `attachmentRegionJoins`, which `PS127`
|
|
2469
|
+
* measures against the loader's own `findRegion` calls, and the region names come
|
|
2470
|
+
* from `parseAtlasText`, which is what `--atlas-in` already resolves against.
|
|
2471
|
+
* Names are compared EXACTLY — as `A08` compares them and as `findRegion`
|
|
2472
|
+
* matches them — so a padded region name is a miss on both sides.
|
|
2473
|
+
*
|
|
2474
|
+
* ⚠️ What it deliberately does not do is catch the pose. A blanket `try` around
|
|
2475
|
+
* `skeletonDataFromText` would convert any exception the runtime raises into a
|
|
2476
|
+
* sentence claiming the cause is missing art, and a rigc defect reported under
|
|
2477
|
+
* somebody else's name is the doctrine's second bullet inverted. This refuses
|
|
2478
|
+
* the case it can NAME, before a line of the report is printed, and leaves
|
|
2479
|
+
* anything else to arrive as itself.
|
|
2480
|
+
*/
|
|
2481
|
+
function refuseUnposableArt(result: CompileResult, opts: CompileOptions): void {
|
|
2482
|
+
const regionNames = parseAtlasText(result.atlasText).pages.flatMap((page) => page.regions.map((region) => region.name));
|
|
2483
|
+
const have = new Set(regionNames);
|
|
2484
|
+
const misses: Array<{ at: string; attachment: string; lookup: string }> = [];
|
|
2485
|
+
let lookups = 0;
|
|
2486
|
+
for (const join of attachmentRegionJoins(JSON.parse(result.skeletonText))) {
|
|
2487
|
+
// A `sequence` this walk will not guess at names no region it can check, and
|
|
2488
|
+
// `A08` passes over it for the same reason.
|
|
2489
|
+
if (join.lookups === null) continue;
|
|
2490
|
+
for (const lookup of join.lookups) {
|
|
2491
|
+
lookups++;
|
|
2492
|
+
if (!have.has(lookup)) {
|
|
2493
|
+
misses.push({
|
|
2494
|
+
at: `skin "${join.skin}" slot "${join.slot}" placeholder "${join.placeholder}"`,
|
|
2495
|
+
attachment: join.name,
|
|
2496
|
+
lookup,
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (misses.length === 0) return;
|
|
2502
|
+
const first = misses[0];
|
|
2503
|
+
// Reported because it was measured, and absent where there is none — `A08`'s
|
|
2504
|
+
// own near-miss clause, in `A08`'s own words.
|
|
2505
|
+
const near = regionNames.find((region) => region.trim().toLowerCase() === first.lookup.toLowerCase());
|
|
2506
|
+
const has =
|
|
2507
|
+
regionNames.length === 0
|
|
2508
|
+
? 'it declares no region at all'
|
|
2509
|
+
: `it declares ${regionNames.length} region(s) and none of them is that${
|
|
2510
|
+
near === undefined ? '' : `, though it does have ${JSON.stringify(near)}`
|
|
2511
|
+
}`;
|
|
2512
|
+
const remedy =
|
|
2513
|
+
opts.atlasInPath === undefined
|
|
2514
|
+
? 'Art reaches a compile two ways and this run took neither: `--atlas-in <pack.atlas>` resolves the parts ' +
|
|
2515
|
+
'against a pack somebody already made, and an "image" per attachment resolves them as loose PNGs under ' +
|
|
2516
|
+
'`--images <dir>` — a spec that states a size and names no image is what `ingest --art none` writes, and ' +
|
|
2517
|
+
'`--atlas-in` is what reads it'
|
|
2518
|
+
: `Either the spec's region name or ${opts.atlasInPath} is the one that moved: fix the name, or point ` +
|
|
2519
|
+
'`--atlas-in` at the pack that has it';
|
|
2520
|
+
throw new ExplainError(
|
|
2521
|
+
`${first.at}: attachment ${JSON.stringify(first.attachment)} wants region ${JSON.stringify(first.lookup)}, ` +
|
|
2522
|
+
`which this build's atlas does not have (${has}). \`explain\` poses the rig to measure its deform keys and a ` +
|
|
2523
|
+
`pose resolves every attachment against the atlas, so there is nothing to pose it against. ${remedy}. ` +
|
|
2524
|
+
`${misses.length} of ${lookups} attachment lookup(s) here resolve to no region.`,
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2437
2528
|
function cmdExplain(flags: Record<string, string>): void {
|
|
2438
2529
|
const { label, opts } = resolveCut(flags);
|
|
2439
2530
|
console.log(`rigc explain ${label}`);
|
|
@@ -2442,6 +2533,13 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
2442
2533
|
console.log(` .. rig ${opts.rigPath}`);
|
|
2443
2534
|
console.log(` .. motion ${opts.motionPath}`);
|
|
2444
2535
|
const result = compile(opts);
|
|
2536
|
+
// Before a line of the report, rather than at the pose two hundred lines in:
|
|
2537
|
+
// the blocks that need the art — DEFORM, meshes, dropped states — all sit
|
|
2538
|
+
// BELOW the pose, so a run that printed the bone and timeline dump and then
|
|
2539
|
+
// refused would be a report missing everything the missing art decides, with
|
|
2540
|
+
// the sentence saying so scrolled off the top. It is the invocation that has
|
|
2541
|
+
// to change, so it is refused before the report it cannot finish (issue #697).
|
|
2542
|
+
refuseUnposableArt(result, opts);
|
|
2445
2543
|
// `compile` has already parsed this file, so the read below cannot fail — but
|
|
2446
2544
|
// it goes through the same parser rather than a cast, because the cast was the
|
|
2447
2545
|
// last one in the repository and issue #307 was about exactly that.
|
|
@@ -2825,14 +2923,30 @@ function cmdIngest(flags: Record<string, string>, positional: string[]): void {
|
|
|
2825
2923
|
console.log(` .. art ${art}`);
|
|
2826
2924
|
if (specImages !== undefined) console.log(` .. images ${specImages} (the rig spec's own, from ${outDir})`);
|
|
2827
2925
|
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2926
|
+
/**
|
|
2927
|
+
* What the run has to report, however the parse went.
|
|
2928
|
+
*
|
|
2929
|
+
* 🔒 A spec the tree's own parser refuses is a **finding**, not an escape
|
|
2930
|
+
* (issue #692): the three files are written, the coded `BLOCK` line is
|
|
2931
|
+
* printed, and the exit code comes off the findings like every other run's.
|
|
2932
|
+
* The two specs are read as `unknown` because that is all this function does
|
|
2933
|
+
* with them — `JSON.stringify` — and a cast to `RigSpec` here would be this
|
|
2934
|
+
* file claiming a parse that did not happen.
|
|
2935
|
+
*/
|
|
2936
|
+
let result: { rig: unknown; motion: unknown; findings: IngestFinding[] };
|
|
2937
|
+
try {
|
|
2938
|
+
result = ingest(readJsonFile(skeletonPath), {
|
|
2939
|
+
name: flags.name ?? basename(skeletonPath, '.json'),
|
|
2940
|
+
art,
|
|
2941
|
+
images: specImages,
|
|
2942
|
+
stage,
|
|
2943
|
+
source: basename(skeletonPath),
|
|
2944
|
+
version: readVersion(),
|
|
2945
|
+
});
|
|
2946
|
+
} catch (err) {
|
|
2947
|
+
if (!(err instanceof IngestSpecRefused)) throw err;
|
|
2948
|
+
result = { rig: err.rig, motion: err.motion, findings: err.findings };
|
|
2949
|
+
}
|
|
2836
2950
|
|
|
2837
2951
|
mkdirSync(outDir, { recursive: true });
|
|
2838
2952
|
// Indent 2, which is what `compile` writes the skeleton with. One emitter
|
|
@@ -3108,8 +3222,20 @@ const COMMANDS: CommandDoc[] = [
|
|
|
3108
3222
|
},
|
|
3109
3223
|
{
|
|
3110
3224
|
name: 'explain',
|
|
3111
|
-
usage: [
|
|
3112
|
-
|
|
3225
|
+
usage: [
|
|
3226
|
+
'rigc explain --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] (it never gates, and writes nothing)',
|
|
3227
|
+
'rigc explain … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made, as build does)',
|
|
3228
|
+
'rigc explain --cut <name> --cuts <cuts.json>',
|
|
3229
|
+
],
|
|
3230
|
+
flags: ['rig', 'motion', 'out', 'manifest', 'images', 'atlas-in', 'cut', 'cuts'],
|
|
3231
|
+
notes: [
|
|
3232
|
+
'this line said "the same arguments as build, minus --profile" and was false in both',
|
|
3233
|
+
'directions (issue #697): --atlas-in was not listed here, so the one flag that lets this',
|
|
3234
|
+
'command read what `ingest --art none` writes was reachable and undocumented, while',
|
|
3235
|
+
'--pack, --page-size, --padding and --copy-images are build\'s and do nothing here —',
|
|
3236
|
+
'they decide what is WRITTEN, and this command writes nothing. What it takes is listed',
|
|
3237
|
+
'above, and that is now the whole of it.',
|
|
3238
|
+
],
|
|
3113
3239
|
},
|
|
3114
3240
|
{
|
|
3115
3241
|
name: 'validate',
|
|
@@ -3482,6 +3608,13 @@ try {
|
|
|
3482
3608
|
console.error(`rigc pose: ${err.message}`);
|
|
3483
3609
|
process.exit(2);
|
|
3484
3610
|
}
|
|
3611
|
+
// A refusal of the invocation, like the two below it, and exit 2 for the same
|
|
3612
|
+
// reason: nothing was posed and nothing was written, so it is the command line
|
|
3613
|
+
// that has to change (issue #697).
|
|
3614
|
+
if (err instanceof ExplainError) {
|
|
3615
|
+
console.error(`rigc explain: ${err.message}`);
|
|
3616
|
+
process.exit(2);
|
|
3617
|
+
}
|
|
3485
3618
|
// Same kind as a PoseError, and printed the same way for the same reason: the
|
|
3486
3619
|
// messages name a path, a bone or an attachment, and reprinting the whole
|
|
3487
3620
|
// usage under them buries the one line that says what to change.
|