spine-rigc 0.32.0 → 0.33.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 CHANGED
@@ -516,7 +516,7 @@ commands take it and what its default is.
516
516
  | `build … --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` — losslessly, and gated a second time as the pair that ships. `--page-size` and `--padding` tune it |
517
517
  | `build … --atlas-in <file.atlas>` | the same build with every part resolved to a **region of an existing pack** instead of a loose PNG; a name the atlas lacks, a size the spec disagrees with or a rectangle off its page is refused by name |
518
518
  | `validate <dir>` | re-gates artifacts already on disk |
519
- | `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. `--stage x,y,w,h` supplies the stage for a skeleton that declares none — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
519
+ | `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
520
520
  | `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
521
521
  | `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/` |
522
522
  | `preview --candidate <dir>` | one self-contained `.html` that plays it |
@@ -595,10 +595,12 @@ One thing it drops on purpose and says so: a path attachment's `lengths`, which
595
595
 
596
596
  ⚠️ **Two values are not in a skeleton at all.**
597
597
 
598
- - **The stage.** `skeleton.width`/`height`: rigc always writes one, and a skeleton that
599
- carries none is refused by name unless `--stage x,y,w,h` supplies it. It is not
600
- derivable — posing the rig gives the *animated* extent, which is a different number
601
- from the setup box. ⛔ **And `--stage` beside a box the file already states is refused
598
+ - **The stage.** `skeleton.width`/`height`. A skeleton that declares none is carried as
599
+ declaring none — the rig spec states `"width": null, "height": null` and the rebuild
600
+ carries no box either, byte for byte
601
+ ([#714](https://github.com/firejune/rigc/issues/714)) — and `--stage x,y,w,h` is how a
602
+ caller *adds* one, recorded as a judgement. It is not derivable — posing the rig gives
603
+ the *animated* extent, which is a different number from the setup box. ⛔ **And `--stage` beside a box the file already states is refused
602
604
  too**, for the opposite reason: two sources for one value, where the file is the record
603
605
  of what was measured. It used to be read after the box and therefore never
604
606
  ([#626](https://github.com/firejune/rigc/issues/626)). ⚠️ This said *"an editor export carries none"* until #594 measured
package/cli.ts CHANGED
@@ -1746,6 +1746,46 @@ function readPositiveNumber(flags: Record<string, string>, key: string, fallback
1746
1746
  return value;
1747
1747
  }
1748
1748
 
1749
+ /**
1750
+ * Does this skeleton declare a setup stage — a numeric width AND height?
1751
+ *
1752
+ * Read off the loaded `SkeletonData` for `render` and off the file's header for
1753
+ * `preview`, which never loads one; both are the same two fields, because
1754
+ * `SkeletonJson` copies them across unconditionally (`SkeletonJson.js:70-73`),
1755
+ * so a header that omits them leaves `undefined` on a field typed `number`.
1756
+ */
1757
+ function declaresSetupStage(header: { width?: unknown; height?: unknown }): boolean {
1758
+ return typeof header.width === 'number' && typeof header.height === 'number';
1759
+ }
1760
+
1761
+ /** The `skeleton` block of a skeleton file's text, or an empty one where it has none. */
1762
+ function skeletonHeaderOf(skeletonText: string): { width?: unknown; height?: unknown } {
1763
+ const root = JSON.parse(skeletonText) as { skeleton?: { width?: unknown; height?: unknown } };
1764
+ return root.skeleton ?? {};
1765
+ }
1766
+
1767
+ /**
1768
+ * What `render` and `preview` say of their framing on a skeleton that declares
1769
+ * no stage (issue #714).
1770
+ *
1771
+ * ⚠️ **Neither frames to a stage on ANY skeleton**, so this is not a fallback
1772
+ * being announced: `framingViewport` is the union of every animation's posed
1773
+ * bounds, and the Spine Web Player's `calculateAnimationViewport` samples the
1774
+ * playing animation's bounds whenever its config states no viewport box, which
1775
+ * `buildPreview` never does. The line is printed only where a stage is absent
1776
+ * because that is the one case where a reader can ask *what box stood in for
1777
+ * it* — and the answer has to be "none", said, rather than a rectangle that looks
1778
+ * like a default. On a staged skeleton the output is the bytes it always was.
1779
+ */
1780
+ const STAGELESS_FRAMING = {
1781
+ render:
1782
+ 'framing the posed extent of every animation, padded — this skeleton declares no stage, and nothing stands ' +
1783
+ 'in for one: render frames to the posed extent whether or not a stage is declared',
1784
+ preview:
1785
+ "framing the Spine Web Player's own: the posed extent of the animation it plays — this skeleton declares no " +
1786
+ 'stage, and nothing stands in for one: the player frames that way whether or not a stage is declared',
1787
+ } as const;
1788
+
1749
1789
  /**
1750
1790
  * render — the frame series, drawn by the same rasteriser `check` measures with.
1751
1791
  *
@@ -1788,6 +1828,7 @@ function cmdRender(flags: Record<string, string>): void {
1788
1828
  const sampled: Map<string, Frame[]> =
1789
1829
  only === undefined ? sampleAll(data, fps, pose) : new Map([[only, sampleAnimation(data, only, fps, pose)]]);
1790
1830
  console.log(` .. ${viewport.width}x${viewport.height}px at ${fps} fps, ${sampled.size} set(s) -> ${outRoot}`);
1831
+ if (!declaresSetupStage(data)) console.log(` .. ${STAGELESS_FRAMING.render}`);
1791
1832
 
1792
1833
  mkdirSync(outRoot, { recursive: true });
1793
1834
  const sets: FrameSet[] = [];
@@ -1903,6 +1944,7 @@ function cmdPreview(flags: Record<string, string>): void {
1903
1944
  for (const page of pages) {
1904
1945
  console.log(` .. page ${page.name.padEnd(28)} ${(page.bytes.length / 1024).toFixed(1)} KiB`);
1905
1946
  }
1947
+ if (!declaresSetupStage(skeletonHeaderOf(skeletonText))) console.log(` .. ${STAGELESS_FRAMING.preview}`);
1906
1948
 
1907
1949
  const html = buildPreview({
1908
1950
  skeletonText,
@@ -3263,9 +3305,10 @@ const FLAG_MEANINGS: Record<string, string> = {
3263
3305
  '--images <dir>` on every rebuild), `none` states width/height only for `build --atlas-in <pack>` to ' +
3264
3306
  'resolve (default: loose)',
3265
3307
  stage:
3266
- 'the setup bounding box — `skeleton.x,y,width,height` — for a skeleton that declares none. It cannot be ' +
3308
+ 'the setup bounding box — `skeleton.x,y,width,height` — to ADD to a skeleton that declares none. It cannot be ' +
3267
3309
  'derived: posing the rig gives the ANIMATED extent, which is a different number from the setup box, so this ' +
3268
- "is the caller's value, and without it the missing stage is reported as a blocker. ⚠️ An editor export MAY " +
3310
+ "is the caller's value, and without it the absence is carried: the spec states `\"width\": null, \"height\": " +
3311
+ 'null` and the rebuild declares no stage either. ⚠️ An editor export MAY ' +
3269
3312
  'carry none; every editor export measured for this project carries one and ingest reads it straight through, ' +
3270
3313
  'so the flag is for a file that really has none rather than for editor exports as a class. ⛔ Beside a ' +
3271
3314
  'skeleton that already declares a box it is REFUSED rather than ignored: two sources for one value, and the ' +
@@ -3677,12 +3720,11 @@ const USAGE = [
3677
3720
  ' rigc ingest hero.json --out specs/ --images parts/ rig.json + motion.json',
3678
3721
  'The contract is an equality, not a rulebook: build(ingest(x)) is x, byte for byte.',
3679
3722
  'It reads the skeleton and nothing else — no .spine project, no binary .skel, no',
3680
- 'atlas — so two things are the caller\'s and are refused rather than guessed: the',
3681
- 'setup stage (--stage, only when the skeleton itself declares none — beside a box the',
3682
- 'file states, the flag is refused rather than ignored) and how the spec',
3683
- 'reaches the art (--art). --images <dir> is the third and the only optional one: it',
3684
- 'WRITES the rig spec\'s own images directory, relative to --out, so the rebuild needs',
3685
- 'no flag.',
3723
+ 'atlas — so how the spec reaches the art is the caller\'s (--art) and is not guessed.',
3724
+ 'A setup stage the skeleton states is read; one it does not state is carried as',
3725
+ 'absent, and --stage is how a caller adds a box to such a file — beside a box the',
3726
+ 'file states, the flag is refused rather than ignored. --images <dir> WRITES the rig',
3727
+ 'spec\'s own images directory, relative to --out, so the rebuild needs no flag.',
3686
3728
  'Everything the spec format cannot hold is printed as a named finding and',
3687
3729
  'exits non-zero, with both files still written, because a spec plus a list of what',
3688
3730
  'is missing from it beats no spec at all.',
package/docs/AUTHORING.md CHANGED
@@ -573,7 +573,7 @@ repository builds on every run.
573
573
  | `--art loose` (default) | name an `image` per attachment — `<path or placeholder>.png` — so the rebuild resolves loose PNGs and rigc measures them |
574
574
  | `--art none` | state `width`/`height` only, so the rebuild is `build --atlas-in <pack.atlas>` and every part resolves out of the pack |
575
575
  | `--images <dir>` | **write** the rig spec's own `images` directory, spelled relative to `--out`, so the rebuild is a plain `build --rig … --motion … --out …`. Without it the field is left out and every `image` resolves against `--out` itself, which holds the specs and no art — so every rebuild has to repeat `build --images <dir>`. Refused together with `--art none`, which writes no `image` for it to be the base of |
576
- | `--stage x,y,w,h` | the setup bounding box, **for a skeleton that declares none**. An editor export *may* be one; every export under `examples/` carries a box and `ingest` reads it straight through — so passing the flag at one of them is **refused**, naming both boxes, rather than silently doing nothing ([#626](https://github.com/firejune/rigc/issues/626)) |
576
+ | `--stage x,y,w,h` | a setup bounding box to **add** to a skeleton that declares none — without it the absence is carried as `"width": null, "height": null` ([#714](https://github.com/firejune/rigc/issues/714)). An editor export *may* be such a file; every export under `examples/` carries a box and `ingest` reads it straight through — so passing the flag at one of them is **refused**, naming both boxes, rather than silently doing nothing ([#626](https://github.com/firejune/rigc/issues/626)) |
577
577
  | `--name <n>` | the rig spec's `name`, which the motion spec's `archetype` must equal (default: the file's basename) |
578
578
 
579
579
  ⚠️ **`ingest --images` and `build --images` point opposite ways.** `build --images`
@@ -586,9 +586,13 @@ where the parts are in one convention.
586
586
  🚨 **The stage is one of the two values `ingest` will not guess.** A skeleton JSON
587
587
  *need not* carry `skeleton.width`/`height`, and when it does not rigc cannot derive
588
588
  one — posing the rig gives the *animated* extent, which is a different number from the
589
- editor's setup box. So a file that declares none is a **blocker**, named, unless
590
- `--stage x,y,w,h` supplies it; supply it from the project the file came from, or from
591
- the editor's own canvas.
589
+ editor's setup box. So a file that declares none is **carried as declaring none**: the
590
+ spec states `"width": null, "height": null` (§3.1), the rebuild emits no box, and
591
+ nothing is recorded, because the rebuild is the file that was read
592
+ ([#714](https://github.com/firejune/rigc/issues/714)). `--stage x,y,w,h` *adds* a box —
593
+ supply it from the project the file came from, or from the editor's own canvas — and
594
+ is recorded as a `NO_STAGE` judgement. A header stating **half** a stage (an origin
595
+ with no extent) is a `NO_STAGE` blocker: the spec holds a stage as four fields or none.
592
596
 
593
597
  ⛔ **The flag is refused beside a box the file states.** Two sources for one value, and
594
598
  the file is the one that was measured — so `ingest` names both boxes and stops rather
@@ -609,7 +613,8 @@ early return on every one of them, and not one needs the flag. What an editor ex
609
613
  round trip with a header of `hash`, `spine`, `images`, `audio` and **no box at all** —
610
614
  the editor preserves the absence rather than inventing a stage
611
615
  ([#616](https://github.com/firejune/rigc/issues/616)). So `--stage` is for a file that
612
- really has none, and this repository's corpus holds no example of one. It is still the
616
+ really has none, and this repository's corpus holds no example of one; a production
617
+ corpus measured for [#714](https://github.com/firejune/rigc/issues/714) holds 48 of 48. It is still the
613
618
  value that costs least to get wrong: `diff` reports the box as `stage_present` and
614
619
  `stage_box` ([#578](https://github.com/firejune/rigc/issues/578)) and both are
615
620
  `(reported)`, so nothing on the ladder consults them.
@@ -1079,6 +1084,23 @@ Three readings stay apart, and the middle one is the point of the other two:
1079
1084
  | one `null`, one number | refused — a stage has both extents or neither, and which half was meant is not derivable |
1080
1085
  | the pair `null` **and** an `x` or `y` | refused — an origin for a box that is not there |
1081
1086
 
1087
+ What each tool does without one — every reader of the stage in the tree, measured
1088
+ for [#714](https://github.com/firejune/rigc/issues/714), and not one of them puts a
1089
+ number where the box would be:
1090
+
1091
+ | Reader | With a stage | Without one |
1092
+ | --- | --- | --- |
1093
+ | `build` | emits `x`/`y`/`width`/`height` | emits none of them |
1094
+ | `A14_NO_FULL_FRAME_MESH` | fails a mesh as big as the stage | **SKIP**, by name |
1095
+ | `A19_OVERLAY_PNGS_HAVE_ALPHA` | exempts the one image that covers the stage | exempts nothing, and says so |
1096
+ | `diff` | `stage_present` and `stage_box` | `stage_present` 1/1 when neither side declares one (agreement), `stage_box` 0/0 |
1097
+ | `explain` | `stage W x H` | `stage none declared` |
1098
+ | `render` | frames the posed extent of every animation | the same frames, plus a line saying the viewport is the posed extent and no stage |
1099
+ | `preview` | the Spine Web Player frames the posed extent of the animation it plays | the same, plus the same line |
1100
+ | `check` | fits the candidate's world box from what it draws | the same figures, plus a note saying so |
1101
+ | `build --pack` | never reads it | the same pages and atlas |
1102
+ | `ingest` | reads it straight through | writes `"width": null, "height": null`; `--stage` adds one |
1103
+
1082
1104
  ⚠️ A stage-less skeleton is **unmeasured, not certified**: `A14_NO_FULL_FRAME_MESH`
1083
1105
  reports **SKIP** on one, because there is no full frame for a mesh to span. And
1084
1106
  `rigc diff` reports it — `skeleton.stage_present` and `skeleton.stage_box`, in the
package/docs/INGEST.md CHANGED
@@ -531,14 +531,23 @@ at the policy rather than implying the file was read.
531
531
 
532
532
  **Two values are not in a skeleton**, so `ingest` asks rather than guesses:
533
533
 
534
- - **the stage** (`skeleton.width`/`height`) — `--stage x,y,w,h` is how you supply one
535
- when the file has none. ⚠️ **This page said an editor export carries none until
534
+ - **the stage** (`skeleton.width`/`height`) — a file that declares none is **carried as
535
+ declaring none**: the rig spec states `"width": null, "height": null` (§2.1 step 3's
536
+ spelling, [#578](https://github.com/firejune/rigc/issues/578)), the rebuild emits a
537
+ header with none of `x`/`y`/`width`/`height`, and it is the file that was read, byte
538
+ for byte. No finding is recorded, because nothing was lost and nobody decided
539
+ anything ([#714](https://github.com/firejune/rigc/issues/714)). `--stage x,y,w,h` is how
540
+ a caller *adds* a box to such a file, and that is a `NO_STAGE` **judgement**. Until
541
+ #714 the absence was a blocker and the flag the only road through it — a number the
542
+ source never stated, on the shape #714 counts in 48 of 48 production exports.
543
+ ⚠️ **This page said an editor export carries none until
536
544
  [#594](https://github.com/firejune/rigc/issues/594) measured it: all twelve exports in
537
545
  the fetched corpus carry a stage**, `ingest` reads it straight through, and not one of
538
546
  them needed the flag. What holds without qualification is that the box cannot be
539
547
  *derived* — posing the rig gives the *animated* extent, which is a different number
540
- from the setup box — so a skeleton that really declares none is a `NO_STAGE` blocker
541
- rather than a guess. It is also the value that costs least to get wrong: `diff`
548
+ from the setup box. 🔸 **Half a stage is still a `NO_STAGE` blocker**: an origin with no
549
+ extent, or one extent without the other, declares no stage and is not the absence
550
+ either, and the rig spec holds a stage as four fields or none. It is also the value that costs least to get wrong: `diff`
542
551
  reports it as two measures of its own (`stage_present`, `stage_box`, since
543
552
  [#578](https://github.com/firejune/rigc/issues/578)) and they are `(reported)`, so
544
553
  nothing on the ladder reads them and an absurd box is green nearly everywhere. The
@@ -609,7 +618,7 @@ is the one failure a comparison of two sets cannot show you.
609
618
  | `HEADER_ORIGIN` | `LOSS` | 0 | the source declares an extent and omits `x`/`y`. Inside a declared extent an omitted origin **is** 0, so the spec states it — and the rebuild then spells two fields the source did not | nothing. Same box, different bytes — which is why byte identity is not the claim for an export that takes this branch |
610
619
  | `HEADER_REDERIVED` | `LOSS` | 0 | `skeleton.spine`: the rebuild writes the version of the runtime rigc links. The line says whether that is the same string the source states | nothing — but read the line: a 4.2 export rebuilds as 4.3 in that one field, and a source from another generation raises `GENERATION_UNSUPPORTED` beside it, which is the blocker about the DATA rather than about the string |
611
620
  | `IK_KEY_FIELD` | `BLOCK` | 1 | a key field on an `ik` timeline that is not part of its shape | check the spelling; an unknown field is dropped from the rebuilt track |
612
- | `NO_STAGE` | `BLOCK` `JUDGE` | 1 | the skeleton declares no stage. It is a blocker with no `--stage`, and a **judgement** — exit 0 — when `--stage x,y,w,h` supplies one, because nothing measured the box you gave it | supply the box from the project the file came from. It cannot be derived: posing the rig gives the animated extent, which is a different number |
621
+ | `NO_STAGE` | `BLOCK` `JUDGE` | 1 | the skeleton declares no stage, and one of two things follows. A **judgement** — exit 0 — when `--stage x,y,w,h` supplied a box, because nothing measured the box you gave it. A **blocker** when the header states **half** a stage — an origin with no extent, or one extent without the other — which the rig spec cannot hold; the detail names the fields it states. A header with **none** of the four is not a finding at all: it is carried as `"width": null, "height": null` and rebuilds byte for byte ([#714](https://github.com/firejune/rigc/issues/714)) | for the judgement, nothing if the box came from the project the file came from. For the blocker, supply the box with `--stage`, or take the stray field(s) out of the source and the absence is carried. It cannot be derived: posing the rig gives the animated extent, which is a different number |
613
622
  | `PATH_LENGTHS` | `LOSS` | 0 | the source states a path attachment's `lengths` and rigc re-measures it as `PathConstraint` does | nothing. Dropping it is the correct reading: the field is the runtime's own four-sample forward difference, not an arc length |
614
623
  | `PATH_TIMELINE` | `BLOCK` | 1 | a path-constraint timeline the motion spec has no track for — it carries position, spacing and mix | transcribe it, or accept that the rebuild plays nothing there |
615
624
  | `PHYSICS_DRIVES_NOTHING` | `LOSS` | 0 | a physics constraint none of whose `x`, `y`, `rotate`, `scaleX`, `shearX` is above 0 — absent, or stated at 0 or below. `PhysicsConstraint.update` applies a component only above 0 (`PhysicsConstraint.js:112`), so it moves no bone, and `build` refuses exactly that shape by name at `A23_PHYSICS_CONSTRAINT_EFFECTIVE` — which, until [#731](https://github.com/firejune/rigc/issues/731), meant the whole rebuild of a file an editor exports was refused over a constraint that did nothing in it. The rig spec **omits** it, together with every timeline keyed to it (a track naming it would be an unknown constraint to the rebuild, refused at compile) and its place on any skin's `physics` list; the detail names each, and the values it did state. Measured on a generated rig through spine-core, posing the source with and without such a constraint differs by **0** on every bone world value — and by at most 9e-8 when it sits on the root, which is the runtime's `modifyWorld` recomputing a local transform it had no reason to, not a component. ⚠️ **One thing does move:** a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation, and the detail says which animation and both lengths | nothing, if it was meant to do nothing. If it was meant to jiggle, the file never said so: give it the component it should drive and it is carried like any other. Where the detail names a shortened animation and the length matters to whatever loops it, key something at the length it had |
@@ -693,7 +702,8 @@ it, and the only differing paths were the name and the `note`.
693
702
  the file: `attachment` simply absent.
694
703
 
695
704
  ⚠️ **If the export's `skeleton` block carries no `x`/`y`/`width`/`height`, write
696
- `"width": null, "height": null` and do not invent one** (issue #578). That shape is
705
+ `"width": null, "height": null` and do not invent one** (issue #578) — which is
706
+ also what `rigc ingest` writes for such a file since #714. That shape is
697
707
  common — the twelve exports in `examples/` all carry the four, and 37 of 37 exports
698
708
  in one production corpus carry none of them — and until the `null` pair existed the
699
709
  only two moves were a made-up stage or a file that could not be transcribed. The
@@ -1389,7 +1399,7 @@ clean page:
1389
1399
  exists to make explicit".** The clause was not wrong that a decompiler meets an
1390
1400
  invention — it was wrong about *which*, and wrong that it is unavoidable: a refusal
1391
1401
  naming the field is what this repository does with a missing number everywhere else,
1392
- and it is what `ingest` does here (§2.0). ⚠️ Not to be confused with the *atlas*
1402
+ and a stated absence is what `ingest` writes where the skeleton has none (§2.0, #714). ⚠️ Not to be confused with the *atlas*
1393
1403
  importer below, which is a different direction and also exists.
1394
1404
 
1395
1405
  ⚠️ **What `ingest` is still not.** It reads skeleton JSON and writes two spec files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,11 +36,12 @@ rigc build --rig specs/rig.json --motion specs/motion.json --images parts/ --out
36
36
  It reads the skeleton — **only** the skeleton — and writes the rig spec and motion
37
37
  spec that rebuild it: **byte for byte for a skeleton rigc emitted**, and for an editor
38
38
  export the weaker claim `diff` measures, with three kinds of benign difference left —
39
- INGEST §2.3. Two values it refuses rather than guessing: the
40
- **stage** (`--stage`, for a skeleton that declares none — an editor export *may* be
41
- one, though every one in the example corpus carries a box, and passing the flag at a
42
- file that declares a box is refused rather than ignored) and each animation's
43
- **duration** (the largest key time, recorded as a finding).
39
+ INGEST §2.3. Two values it will not guess: the
40
+ **stage** (a skeleton that declares none is carried as declaring none, and `--stage`
41
+ adds a box to one — an editor export *may* be such a file, though every one in the
42
+ example corpus carries a box, and passing the flag at a file that declares a box is
43
+ refused rather than ignored) and each animation's **duration** (the largest key time,
44
+ recorded as a finding).
44
45
  Read `findings.json`: a `BLOCK` line means the rebuild will be missing something and
45
46
  the command exits non-zero. Every code it can print — gutter, exit, meaning, what to
46
47
  do — is the finding-code table in INGEST §2.0. Keep the `note` both specs carry.
package/src/check.ts CHANGED
@@ -1059,6 +1059,18 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
1059
1059
  const notes: string[] = [];
1060
1060
 
1061
1061
  const posable = posableFromText(options.skeletonText, options.atlasText, options.atlasDir);
1062
+ // A candidate that declares no stage is framed exactly as one that does,
1063
+ // because no framing here reads a stage: the world box is fitted from what the
1064
+ // candidate draws. Said on the one candidate where a reader can ask what box
1065
+ // stood in for the absent one (issue #714) — `SkeletonJson` copies the header
1066
+ // fields across unconditionally (`SkeletonJson.js:70-73`), so an omitted
1067
+ // extent is `undefined` here, not 0.
1068
+ if (typeof posable.data.width !== 'number' || typeof posable.data.height !== 'number') {
1069
+ notes.push(
1070
+ 'the candidate declares no stage (no `skeleton.width`/`height`), and nothing stands in for one: its world ' +
1071
+ 'box is fitted from the pixels it draws, as it is for every candidate, so the absence moves no figure below.',
1072
+ );
1073
+ }
1062
1074
  // The substitution is loaded before anything is posed, because posing has to
1063
1075
  // record each piece's original-art UVs for it and that is the one thing about
1064
1076
  // this measure that cannot be added afterwards — see `PieceTexture`.
package/src/ingest.ts CHANGED
@@ -24,9 +24,11 @@
24
24
  * something nobody wrote. Where the skeleton cannot answer, this records a
25
25
  * **finding** with a code and writes nothing: `findings` is the product, not a
26
26
  * log. Exactly two values are not in a skeleton at all (the stage and an
27
- * animation's duration) and both are `judgement` findings; every construct the
28
- * spec format cannot hold is a `blocker`; everything rigc re-derives rather than
29
- * carries is `lossy`. The one thing it refuses outright rather than recording is
27
+ * animation's duration), and a value somebody supplies for either is a
28
+ * `judgement` finding — a stage the file does not declare is carried as the
29
+ * absence it is, and only a caller's `--stage` is somebody deciding (issue
30
+ * #714); every construct the spec format cannot hold is a `blocker`; everything
31
+ * rigc re-derives rather than carries is `lossy`. The one thing it refuses outright rather than recording is
30
32
  * an OPTION that contradicts the file — see `IngestError`.
31
33
  *
32
34
  * ## What it does not read
@@ -120,8 +122,8 @@ export class IngestSpecRefused extends Error {
120
122
  *
121
123
  * - `blocker` — the spec format cannot say this, so the rebuilt skeleton will
122
124
  * NOT be the one that was read. Non-zero exit.
123
- * - `judgement` — the skeleton does not carry it and somebody has to decide.
124
- * There are exactly two: the stage, and an animation's duration.
125
+ * - `judgement` — the skeleton does not carry it and somebody decided. There
126
+ * are exactly two: a stage the caller supplied, and an animation's duration.
125
127
  * - `lossy` — the skeleton's spelling and rigc's differ, on purpose, and the
126
128
  * difference is named: a value rigc re-derives rather than takes (`lengths`,
127
129
  * the `spine` version), a field the spec has no home for (`hash`, `audio`), or
@@ -646,8 +648,8 @@ export function ingest(skeleton: unknown, opts: IngestOptions): IngestResult {
646
648
  // -- header ---------------------------------------------------------------
647
649
  // 🚨 THE SEAM. One function decides the rig spec's `skeleton` block, and the
648
650
  // stage is the only value in this whole module that a skeleton cannot answer
649
- // for. Issue #578 is landing a way for a rig spec to SAY that a skeleton
650
- // declares no stage; when it does, this is the one place that changes.
651
+ // for. Since #578 a rig spec can SAY that a skeleton declares no stage, and
652
+ // since #714 this is where a file that declares none is carried as saying so.
651
653
  const rigHeader = ingestHeader(obj(root.skeleton), opts, note);
652
654
 
653
655
  // -- physics constraints that drive nothing (issue #731) ------------------
@@ -1014,6 +1016,9 @@ function readGeneration(root: JsonObject, note: Note): void {
1014
1016
  );
1015
1017
  }
1016
1018
 
1019
+ /** The four fields a stage is, in the order the editor and `compile` write them. */
1020
+ const STAGE_FIELDS = ['x', 'y', 'width', 'height'] as const;
1021
+
1017
1022
  /**
1018
1023
  * Does this header declare a stage?
1019
1024
  *
@@ -1043,18 +1048,28 @@ function spellStage(x: unknown, y: unknown, width: unknown, height: unknown): st
1043
1048
  /**
1044
1049
  * The rig spec's `skeleton` block — and the one judgement in this module.
1045
1050
  *
1046
- * 🚨 **A skeleton JSON need not carry the stage, and it cannot be derived.** rigc
1047
- * always emits `x`/`y`/`width`/`height`, so a rigc build round-trips with nothing
1048
- * to decide. `compile` refuses without one, and there is no derivation: posing
1049
- * the rig gives the ANIMATED extent, which is a different number from the
1050
- * editor's setup box. So with no `--stage` this records a blocker naming the
1051
- * field and writes nothing plausible.
1051
+ * 🚨 **A skeleton JSON need not carry the stage, and it cannot be derived.**
1052
+ * Posing the rig gives the ANIMATED extent, which is a different number from the
1053
+ * editor's setup box. So a file that declares none is written as declaring none
1054
+ * — `"width": null, "height": null`, the rig spec's spelling for that claim since
1055
+ * issue #578 — and `compile` then emits a header with none of the four fields,
1056
+ * which is the file that was read, byte for byte. No finding: nothing was lost,
1057
+ * nothing re-derived and nobody decided anything, and a line saying so would be
1058
+ * a finding about a file that rebuilds exactly (issue #714).
1059
+ *
1060
+ * ⚠️ **That is the shape of a whole production corpus, not a corner.** Until
1061
+ * #714 this branch was a `NO_STAGE` blocker and the only road through it was a
1062
+ * caller's `--stage` — a number the source never stated. Issue #714 counts 48 of
1063
+ * 48 production exports at 4.3.26 carrying no box; all twelve exports under
1064
+ * `examples/` carry all four fields, and take the declared branch below.
1052
1065
  *
1053
- * ⚠️ This said an editor export's `skeleton` block is `hash`, `spine`, `images`,
1054
- * `audio` **and no box at all** until issue #594 measured the corpus: all twelve
1055
- * exports under `examples/` carry `x`/`y`/`width`/`height`, and the declared-stage
1056
- * branch below is the one they take. The blocker is for a file that really has
1057
- * none, and this module has no example of one.
1066
+ * 🔸 **Half a stage is still a blocker, and keeps the code.** A header that
1067
+ * states an origin with no extent, or one extent without the other, declares no
1068
+ * stage — the extent is what declares one — but it is not the absence either:
1069
+ * the rig spec holds a stage as four fields or none (`parseRigSpec` refuses the
1070
+ * pair `null` beside an `x`, and one extent alone is `compile`'s `no stage size`),
1071
+ * so the rebuild cannot carry what the file states. No export measured here has
1072
+ * that shape; the blocker names the fields it does state.
1058
1073
  *
1059
1074
  * ⭐ It is still the judgement that costs least to get wrong. `diff` does report
1060
1075
  * the box — `stage_present` and `stage_box`, since issue #578 — but they sit in
@@ -1135,13 +1150,28 @@ function ingestHeader(head: JsonObject, opts: IngestOptions, note: Note): JsonOb
1135
1150
  return out;
1136
1151
  }
1137
1152
  if (opts.stage === undefined) {
1153
+ const stated = STAGE_FIELDS.filter((field) => head[field] !== undefined);
1154
+ if (stated.length === 0) {
1155
+ // The absence, carried: the pair goes where `RIG_KEYS` orders it, so the
1156
+ // spec reads like one a transcriber would have written by hand.
1157
+ const carried: JsonObject = {};
1158
+ for (const field of RIG_KEYS.RigSkeletonHeader) {
1159
+ if (field === 'width' || field === 'height') carried[field] = null;
1160
+ else if (out[field] !== undefined) carried[field] = out[field];
1161
+ }
1162
+ return carried;
1163
+ }
1164
+ const unstated = STAGE_FIELDS.filter((field) => head[field] === undefined);
1138
1165
  note(
1139
1166
  'blocker',
1140
1167
  'NO_STAGE',
1141
1168
  'skeleton.width/height',
1142
- 'the skeleton declares no stage; give --stage x,y,w,h — the value is the caller\'s, not derived. Posing the ' +
1143
- 'rig would give the ANIMATED extent, which is a different number from the setup box, so rigc refuses rather ' +
1144
- 'than measuring the wrong thing (or state the absence once the spec can)',
1169
+ `the skeleton states ${stated.map((field) => `"${field}"`).join(', ')} and no ` +
1170
+ `${unstated.map((field) => `"${field}"`).join(', ')}, so it declares no stage — a width and a height are ` +
1171
+ 'what declare one — and it is not the absence either. A rig spec holds a stage as four fields or none, so ' +
1172
+ 'the rebuild cannot carry what this header states. Give --stage x,y,w,h if the box is known — the value is ' +
1173
+ 'the caller\'s, not derived: posing the rig would give the ANIMATED extent, which is a different number from ' +
1174
+ 'the setup box — or take the stray field(s) out of the source, and the absence is then carried as it stands',
1145
1175
  );
1146
1176
  return out;
1147
1177
  }
@@ -1151,8 +1181,9 @@ function ingestHeader(head: JsonObject, opts: IngestOptions, note: Note): JsonOb
1151
1181
  'NO_STAGE',
1152
1182
  'skeleton.width/height',
1153
1183
  `the skeleton declares no stage and the caller supplied ${opts.stage.x},${opts.stage.y},${opts.stage.width},` +
1154
- `${opts.stage.height}. Nothing measured it: no gate in this tree reads the skeleton header, so a wrong box is ` +
1155
- 'green everywhere (or state the absence once the spec can)',
1184
+ `${opts.stage.height}. Nothing measured it against the art: \`A14\` and \`A19\` measure the art against it ` +
1185
+ 'and `diff` reports it, so a wrong box is green everywhere. Without --stage the absence is carried instead, ' +
1186
+ 'and the rebuild declares no stage either',
1156
1187
  );
1157
1188
  return out;
1158
1189
  }