spine-rigc 2.49.0 → 2.50.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
@@ -619,13 +619,13 @@ and `bun run fetch-examples`. The reasoning behind them is in
619
619
  [the benchmark dossier](https://github.com/rig-c/core/blob/main/docs/BENCHMARK.md).
620
620
 
621
621
  `build` and `validate` both default to `--profile spine` — the 35 validity rules, which
622
- ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
622
+ ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile rig-play`
623
623
  adds all 51: the other 16 are one renderer's policy and one canvas budget's, and they
624
624
  fire on perfectly correct editor-produced Spine data, which is why they are opt-in.
625
625
  ⇒ **That reason is about foreign data and does not carry to a rig you are authoring
626
- yourself: author under `--profile spine-html` and read the extra 16 as findings, and
626
+ yourself: author under `--profile rig-play` and read the extra 16 as findings, and
627
627
  gate the release under `--profile spine`.** A `deform` key that folds a mesh inside
628
- out is written out under the default and refused by name under `spine-html`, which
628
+ out is written out under the default and refused by name under `rig-play`, which
629
629
  is the shape of what that split buys you. A report always names
630
630
  the profile it ran and lists what that profile left out.
631
631
 
package/docs/AUTHORING.md CHANGED
@@ -239,7 +239,7 @@ What the flags mean:
239
239
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
240
240
  | `--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 |
241
241
  | `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
242
- | `--profile` | `spine` = the 35 validity rules (**the default**) · `spine-html` = all 51, opt-in |
242
+ | `--profile` | `spine` = the 35 validity rules (**the default**) · `rig-play` = all 51, opt-in. `spine-html`, the profile's former name, is read as `rig-play` until the next minor release and prints a deprecation line on stderr naming `rig-play` |
243
243
  | `--report` | `build` and `repack`: also write the gate's report as a JSON document (`build-report/1`) to this file — every gate's `PASS`, `SKIP` and `FAIL` rows, its summary figures and stats line, which supplier judged it, and every `pack:` line's figures. Written when the command writes `--out` and when a gate is red, before the exit; a file already at the path is removed first, so a compile error or a refused repack leaves none. Never inside `--out`, refused by name. The printed lines are byte-identical with it and without it — **§5.3** |
244
244
  | `--candidate` | `check`, `bench`, `render`, `inspect`, `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 |
245
245
  | `--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 |
@@ -306,7 +306,7 @@ reported `already linked`.
306
306
 
307
307
  Pick the profile deliberately, and know which one you got by saying nothing. The
308
308
  default is `spine`: "is this valid Spine 4.3 that any runtime plays correctly",
309
- which is the question a rig authored anywhere is asking. `--profile spine-html`
309
+ which is the question a rig authored anywhere is asking. `--profile rig-play`
310
310
  adds one renderer's policy and one project's canvas budget on top, and those
311
311
  extra rules fire on perfectly correct Spine data (clipping attachments,
312
312
  unweighted meshes, packed atlases) — reach for it when you are shipping into
@@ -314,8 +314,8 @@ unweighted meshes, packed atlases) — reach for it when you are shipping into
314
314
  lists what that profile left out, on `PROF` lines.
315
315
 
316
316
  *Packed atlases* are in that list for what a page may hold, not for being packed:
317
- under `spine-html` a page is **one part per page OR a tiling page**, so `--pack
318
- --profile spine-html` is an ordinary build — and it is the only one that puts the
317
+ under `rig-play` a page is **one part per page OR a tiling page**, so `--pack
318
+ --profile rig-play` is an ordinary build — and it is the only one that puts the
319
319
  renderer's own rulebook over shared-page sampling. What a *tiling* page has to
320
320
  satisfy under that profile is §5.2's `A49` row: no two regions on one page
321
321
  drawing over each other — refused where their rectangles overlap **and** their
@@ -600,7 +600,7 @@ what changes is that two rectangles may overlap where neither footprint is.
600
600
  against 155 and 44, all by 1, between their own `rect` packs under `pot` and
601
601
  `free`. No texel is read from a neighbour (that is three orders of magnitude
602
602
  louder, `PK06`).
603
- - **The gate reads footprints, not rectangles.** Under `--profile spine-html`
603
+ - **The gate reads footprints, not rectangles.** Under `--profile rig-play`
604
604
  `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` refuses two regions on one page only
605
605
  where their rectangles overlap **and** what they draw overlaps — a mesh's
606
606
  hull (its UVs over the region's bounds; a linked mesh its source's), a region
@@ -928,7 +928,7 @@ report carries the rectangle to cut instead of leaving it to be derived:
928
928
 
929
929
  - `--atlas-in` cannot recover what a `scale:` quantised away (above), turned or not;
930
930
  - the **renderer** profile refuses a turned region outright
931
- (`A06`, `--profile spine-html`), because that profile is about artifacts rigc
931
+ (`A06`, `--profile rig-play`), because that profile is about artifacts rigc
932
932
  itself emits and it never packs one turned. Reading a foreign pack and gating
933
933
  one under somebody else's renderer policy are different questions.
934
934
 
@@ -2057,7 +2057,7 @@ and the inheritance silently falls back to Normal — assertion `A02` refuses it
2057
2057
  | `bone` | required; must be a bone this rig declares | — |
2058
2058
  | `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) — **except** on a slot nothing fills, where it can only be `null` and may be left out |
2059
2059
  | `color` | `rrggbbaa` tint | opaque white |
2060
- | `dark` | two-colour tint, `rrggbb`. The **setup** half; §4.4's `rgba2` and `rgb2` tracks key it over time and require it | — (🚫 `A12` under `spine-html`) |
2060
+ | `dark` | two-colour tint, `rrggbb`. The **setup** half; §4.4's `rgba2` and `rgb2` tracks key it over time and require it | — (🚫 `A12` under `rig-play`) |
2061
2061
  | `blend` | `normal` · `additive` · `multiply` · `screen`. Only the first letter's case is free: `Additive` reads as `additive`, while `ADDITIVE` or `mUlTiPlY` reads as **no mode** in the runtime with no error, so `build` refuses it by name (§5.1). Emitted as written | `normal` |
2062
2062
 
2063
2063
  ✅ **Every slot you declare is emitted, in this order.** A slot nothing fills — no
@@ -2119,7 +2119,7 @@ name no `image` — an empty `default`, or one carrying only a `boundingbox`, a
2119
2119
  with **no pages**, and rigc writes `skeleton.atlas` as an **empty file** (zero
2120
2120
  bytes). Four rules then report SKIP by name rather than a pass, because a page is
2121
2121
  their whole subject: `A07_ATLAS_TEXT_SHAPE`, `A06_ATLAS_PAGE_SIZE_MATCHES_PNG`,
2122
- `A17_ATLAS_PAGE_FILES_EXIST` and — under `spine-html` —
2122
+ `A17_ATLAS_PAGE_FILES_EXIST` and — under `rig-play` —
2123
2123
  `A19_OVERLAY_PNGS_HAVE_ALPHA` and `A27_REGION_NAME_MATCHES_PAGE_FILENAME`.
2124
2124
 
2125
2125
  ⚠️ **This is not the case where an attachment WANTS a region.** A region or mesh
@@ -2458,8 +2458,8 @@ attachment rather than a second set of art.
2458
2458
  | `inverse` | default **false**. True makes everything **outside** the polygon visible instead of everything inside, and inverse clipping is always treated as convex (`ClippingAttachment.inverse`) |
2459
2459
 
2460
2460
  ⚠️ **A clipping attachment is refused by the renderer profile and by nothing
2461
- else.** `A11_NO_CLIPPING_ATTACHMENTS` (§5.2) fires under `--profile spine-html`
2462
- because that renderer skips clipping silently; it is one renderer's policy rather
2461
+ else.** `A11_NO_CLIPPING_ATTACHMENTS` (§5.2) fires under `--profile rig-play`
2462
+ because the renderer it was written for, spine-html 0.4.1, skipped clipping silently; it is one renderer's policy rather
2463
2463
  than anything about the data, and the default `spine` profile builds one.
2464
2464
 
2465
2465
  **Path** ([Spine: paths](http://esotericsoftware.com/spine-paths)) —
@@ -2562,7 +2562,7 @@ window off the attachment's own `image`; a `ring` and a `ribbon` are built from
2562
2562
  the numbers in this block and rigc compares neither to the PNG. The emitted
2563
2563
  `width`/`height` are this `size`, so a `size` that is not the art's own scales the
2564
2564
  drawing — measured: a 240x240 part declared `"size": [64, 64]` builds green under
2565
- `--profile spine-html`. That is R1 rather than a gap: the compiler emits the
2565
+ `--profile rig-play`. That is R1 rather than a gap: the compiler emits the
2566
2566
  number the spec states and does not re-measure a plate to overrule it.
2567
2567
 
2568
2568
  ⭐ **Several grips split the ring by where the rig put them, on this route as on
@@ -3152,7 +3152,7 @@ one 96x64 plate of two islands under two two-link chains, `cell: 8`,
3152
3152
  The `influence` line is the falloff's report: the most bones any vertex binds
3153
3153
  (read it against `maxBones`), the mean, and how many vertices one bone owns
3154
3154
  outright. A named bone that **no** vertex binds is listed there by name — it is
3155
- in no weight, so no other figure moves — and under `--profile spine-html`
3155
+ in no weight, so no other figure moves — and under `--profile rig-play`
3156
3156
  `A20_MESH_WEIGHTS_COHERENT` refuses it as a declared bone nothing binds. The
3157
3157
  `lattice` line says how many cells had art and how many were kept, and when
3158
3158
  islands were joined, how many and how many cells the join added. The coverage
@@ -4413,13 +4413,13 @@ attachment, rgba, rgb, alpha, rgba2, rgb2)`, §5.1's row.
4413
4413
  has one, so a file keying it without one parses cleanly and then throws in the
4414
4414
  player the first time the animation is applied. `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN`
4415
4415
  (§5.2) holds the same pairing on a skeleton rigc did not write. 🚫 Both halves
4416
- of the two-colour tint are refused under `--profile spine-html`, whose renderer
4416
+ of the two-colour tint are refused under `--profile rig-play`, whose DOM mode
4417
4417
  ignores them; the default `spine` profile that `build` runs reports `A12` as
4418
4418
  `PROF` and never applies it.
4419
4419
  - The six are every slot timeline the format has; anything else is refused
4420
4420
  here, and would be refused by the runtime's own reader too (`Invalid timeline
4421
4421
  type for a slot`). `A12_NO_DARK_COLOR` refuses `rgb2` — and `rgba2`, and the
4422
- slot field — in a file under the `spine-html` profile.
4422
+ slot field — in a file under the `rig-play` profile.
4423
4423
  `sequence` is a timeline on an **attachment**, not on a slot — it is the
4424
4424
  family beside `deform`, §4.13.
4425
4425
 
@@ -6485,7 +6485,7 @@ name or point the flag at the pack that has it.
6485
6485
 
6486
6486
  The report prints one line per assertion:
6487
6487
 
6488
- **No run reproduces this:** assembled — one line of each verdict kind; the `PASS` and the `PROF` are verbatim from a `build` this page states, the `SKIP` is cut at the ellipsis and only `--profile spine-html` prints it, and the `FAIL` is invented, because a green build prints none
6488
+ **No run reproduces this:** assembled — one line of each verdict kind; the `PASS` and the `PROF` are verbatim from a `build` this page states, the `SKIP` is cut at the ellipsis and only `--profile rig-play` prints it, and the `FAIL` is invented, because a green build prints none
6489
6489
  ```
6490
6490
  PASS A08_REGION_NAMES_MATCH_ATTACHMENTS
6491
6491
  SKIP A21_MESH_RIM_PINNED: the skeleton has no weighted mesh attachment, …
@@ -6561,13 +6561,13 @@ Fix A00 and run it again.
6561
6561
  | <a id="A03_REGION_WIDTH_HEIGHT_FINITE"></a>`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 |
6562
6562
  | <a id="A04_MESH_TRIANGLES_AND_ENCODING"></a>`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 |
6563
6563
  | <a id="A05_CURVE_ARRAY_LENGTH"></a>`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`). |
6564
- | <a id="A06_ATLAS_PAGE_SIZE_MATCHES_PNG"></a>`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` and rotation. Two regions on one page over the same texels was a clause here until issue #1099 and is `A49`'s now, read over what the two regions draw rather than over their rectangles. **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`, or names the dimension when its `IHDR` states a width or height of **0**, which the format forbids (§0.2). **Or** the file reads as a PNG and its **image data cannot be decoded** — its compressed stream is inflated (not unfiltered) and the decoder refuses it, or it inflates short of the rows the header states — and the message names the file and carries the decoder's own words (`the decoder stops with "incorrect data check"`). That clause is validity too: nothing in rigc can read such a page back — and its failure is the **one** line that names such a page: `A19` SKIPs over it and points here rather than naming it again, the way `A17` alone names a missing file. **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 |
6564
+ | <a id="A06_ATLAS_PAGE_SIZE_MATCHES_PNG"></a>`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 `rig-play` also: `pma` and rotation. Two regions on one page over the same texels was a clause here until issue #1099 and is `A49`'s now, read over what the two regions draw rather than over their rectangles. **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`, or names the dimension when its `IHDR` states a width or height of **0**, which the format forbids (§0.2). **Or** the file reads as a PNG and its **image data cannot be decoded** — its compressed stream is inflated (not unfiltered) and the decoder refuses it, or it inflates short of the rows the header states — and the message names the file and carries the decoder's own words (`the decoder stops with "incorrect data check"`). That clause is validity too: nothing in rigc can read such a page back — and its failure is the **one** line that names such a page: `A19` SKIPs over it and points here rather than naming it again, the way `A17` alone names a missing file. **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 |
6565
6565
  | <a id="A07_ATLAS_TEXT_SHAPE"></a>`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/rig-c/core/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`). |
6566
- | <a id="A08_REGION_NAMES_MATCH_ATTACHMENTS"></a>`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 |
6566
+ | <a id="A08_REGION_NAMES_MATCH_ATTACHMENTS"></a>`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 `rig-play` 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 |
6567
6567
  | <a id="A09_ANIMATION_DURATION_MATCHES_SPEC"></a>`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 |
6568
6568
  | <a id="A10_NO_NAN_AFTER_STEPPING"></a>`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 |
6569
6569
  | <a id="A11_NO_CLIPPING_ATTACHMENTS"></a>`A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
6570
- | <a id="A12_NO_DARK_COLOR"></a>`A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
6570
+ | <a id="A12_NO_DARK_COLOR"></a>`A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored by rig-play's DOM mode. The rule is worded for that mode alone: rig-play's canvas mode carries `dark` on its frame, but no test of the player's holds that it draws it, so nothing here says it does |
6571
6571
  | <a id="A13_MESH_BUDGET"></a>`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 |
6572
6572
  | <a id="A14_NO_FULL_FRAME_MESH"></a>`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* |
6573
6573
  | <a id="A15_IDLE_NO_MESH_BONE_KEYS"></a>`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 |
@@ -6575,7 +6575,7 @@ Fix A00 and run it again.
6575
6575
  | <a id="A17_ATLAS_PAGE_FILES_EXIST"></a>`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` |
6576
6576
  | <a id="A18_DETERMINISTIC_EMIT"></a>`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`). |
6577
6577
  | <a id="A19_OVERLAY_PNGS_HAVE_ALPHA"></a>`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`). On a rig-spec build the sentence goes on *"this skeleton declares no stage size to measure one against, and the build names no base plate"* and offers *"build from a cut manifest, whose base plate is the part whose window is the crop"*; a manifest build exempts the plate it names, with a stage or without one. 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. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page carrying a part was not read and nothing else here failed: the reason names the pages not read and how many of the pages carrying a part were (or says none was), and for each page points at the one rule that names its file — `A17` when the file is **not on disk**, `A06` when it is on disk and **cannot be read as PNG** (not a PNG, truncated, an `IHDR` dimension of 0, or image data that does not decode; `A06` fails naming what the file is on every such page, under both profiles). Undecodable data is found where this rule decodes, so the header's fast negative above still refuses a part with no alpha channel and no `tRNS` without opening it — a verdict about the header, not about the pixels. So one unreadable file is one red line, never two: with another `A19` failure standing, the unread page is that rule's alone and this one says nothing about it. A page holding only the base plate moves nothing here, missing or unreadable: this rule had no part on it to read. |
6578
- | <a id="A20_MESH_WEIGHTS_COHERENT"></a>`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 |
6578
+ | <a id="A20_MESH_WEIGHTS_COHERENT"></a>`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 `rig-play` 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 |
6579
6579
  | <a id="A21_MESH_RIM_PINNED"></a>`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 |
6580
6580
  | <a id="A22_MESH_UVS_IN_UNIT_RANGE"></a>`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 |
6581
6581
  | <a id="A23_PHYSICS_CONSTRAINT_EFFECTIVE"></a>`A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too — or has `damping` outside `[0, 1]`, which says `` physics "C" has damping 1.5; must be inside [0, 1] — the per-step decay is `damping ** (60 * step)`, and the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: above 1 every velocity grows on every step and the offset diverges ``, the bound read off the row the key's refusal reads; `1` and `0` are inside, since both are finite at every rate — **at rest, and on every physics timeline key**. The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps. As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `` physics "C" has mix 0 and none of the 3 animations keys its mix above 0; the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted — rest it above 0, or key its mix above 0 in an animation ``. ⚠️ Every reason A23 prints for a setup value is that value's row `basis` arm (§4.4's table) — an arithmetic arm names its expression (`physics "C" has massInverse Infinity; mass must be > 0 — at 0 …`), a behavioural one says it is rigc's call and what the value does. A setup `mix` **below** 0 is told the jiggle is applied inverted, not that it is muted: such a constraint is measured moving its bone by exactly the opposite of a positive mix. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md)), so this sentence is met on a file, never on a decompiled rebuild. 🦴 **A lever drive on a bone with no lever** (issue #1195): a constraint driving `rotate`, `shearX` or `scaleX` on a bone whose `length` is 0 (or states none) is refused, at rest, with one sentence per reading. `rotate` and `shearX` chase the bone's tip, so with no tip the solver aims the bone at a **world** direction — measured through rigc's poser on a chain held still for 3 s, a length-0 bone turns 53.204° from a 90° rest and holds there, where length 0.01 moves 0.0000: `` physics "C" drives rotate on bone "B" whose length is 0 — with no tip the solver aims the bone at world angle 0 and holds it there; give "B" a length or drive x/y instead ``. The rest angle is not read: a bone resting at world 0° does not move, but only until a key or a parent turns it. `scaleX` divides the motion along the bone by its length and adds nothing at 0, so the drive does nothing: `` physics "C" drives scaleX on bone "B" whose length is 0, which the solver divides the motion by, so it drives nothing; give "B" a length ``. Fix in the **rig spec**: give the bone its `length` (§3.2), measured off the art it carries, or, for the first sentence, drive the constraint in `x`/`y` in the **motion spec** instead. `x` and `y` read the bone's origin and never its tip, so a constraint driving only those on a length-0 bone is not refused; the editor's own example exports carry them there. The Spine editor's export drops `rotate`, `scaleX` and `shearX` from a physics constraint on a zero-length bone and keeps them on any bone with a length — measured on 4.3.23 and 4.3.26, issue #1196 — so what this clause refuses is also what the editor would silently lose. **SKIP** when the skeleton declares no physics constraint — the same sentence `A36` and `A37` print for their own constraint types |
@@ -6738,7 +6738,7 @@ Two more limits that are not errors but will shape what you can attempt:
6738
6738
  1. `build --profile <the one you meant>` exits 0 and the report has **no FAIL**.
6739
6739
  Saying nothing means `spine`, so "the one you meant" is a decision either way —
6740
6740
  the report's first line names the profile that judged it.
6741
- 📎 The two profiles judge attachment **naming** identically: `spine-html` adds
6741
+ 📎 The two profiles judge attachment **naming** identically: `rig-play` adds
6742
6742
  the renderer and archetype rules and has no opinion about how an attachment is
6743
6743
  spelled, so a rig whose named skins share a placeholder (§3.4.9) is green
6744
6744
  under either.
@@ -6759,7 +6759,7 @@ Two more limits that are not errors but will shape what you can attempt:
6759
6759
  3. Read the `PROF` lines. They are where "was this rig held to that rule at all"
6760
6760
  gets answered for everything the profile left out — the renderer policy *and*
6761
6761
  the archetype rules. A green under `spine` has been held to neither; a green
6762
- under `spine-html` has been held to both.
6762
+ under `rig-play` has been held to both.
6763
6763
  4. Run `explain` and read the slots table: every slot you declared **is** there
6764
6764
  (§3.3 — `A26_SLOT_DRAW_ORDER` holds that, and a slot nothing fills reads
6765
6765
  `setup=null attachments=[]`), so what this step is for is the two things the
package/docs/FACE.md CHANGED
@@ -539,7 +539,7 @@ which `rigc explain` prints as a parent column (§9.2).
539
539
  ⭐ **The same reasoning puts the head's pivot one link above the mesh.** A head
540
540
  rotates about the top of the neck, not about the middle of its own face, so the
541
541
  roll belongs on a `headroll` bone above `head`. There is a rule that enforces it
542
- under `--profile spine-html`: `A15_IDLE_NO_MESH_BONE_KEYS` refuses an `idle` that
542
+ under `--profile rig-play`: `A15_IDLE_NO_MESH_BONE_KEYS` refuses an `idle` that
543
543
  keys a bone driving a mesh — its own slot bone or its control bone — because that
544
544
  renderer must never idle-skip a mesh. Keying the pivot one link up satisfies it,
545
545
  and **the rig that satisfies the assertion is the better rig anyway.**
@@ -852,7 +852,7 @@ in the experiment came close to that ratio.
852
852
 
853
853
  ⚠️ **A `clipping` attachment would lift the iris's travel ceiling and you cannot
854
854
  have one.** Spine has them (AUTHORING §3.4) but `A11_NO_CLIPPING_ATTACHMENTS`
855
- refuses one under `--profile spine-html`, so a rig carrying one builds on only one
855
+ refuses one under `--profile rig-play`, so a rig carrying one builds on only one
856
856
  of the two profiles. The ceiling is then geometric: in the worked example the
857
857
  iris's ink ring has radius 28.5 against a socket opening 36 half-wide, so
858
858
  `36 − 28.5 = 7.5` units is as far as it can travel before it crosses its own lash.
@@ -1473,7 +1473,7 @@ bun install # once
1473
1473
 
1474
1474
  bun cli.ts build --rig gallery/portrait/rig.json \
1475
1475
  --motion gallery/portrait/motion.json \
1476
- --out gallery/portrait/build --profile spine-html
1476
+ --out gallery/portrait/build --profile rig-play
1477
1477
  bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
1478
1478
  --out gallery/portrait/render
1479
1479
  ```
@@ -1481,8 +1481,8 @@ bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
1481
1481
  ```
1482
1482
  MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
1483
1483
  MESH hair_bang authored 15 vertices / 16 triangles (budget 32) bones=[bang] attachments=[hair_bang] covers 100.00% of the art, reaching 55.22px past it
1484
- .. validate (spine-core round trip + machine assertions, profile spine-html)
1485
- .. profile spine-html — every assertion applies
1484
+ .. validate (spine-core round trip + machine assertions, profile rig-play)
1485
+ .. profile rig-play — every assertion applies
1486
1486
  ```
1487
1487
 
1488
1488
  Green — the profile line above says so in the tool's own words.
@@ -1515,7 +1515,7 @@ for (const k of d.keys) if (k.transform) {
1515
1515
  await Bun.write("/tmp/swapped.motion.json", JSON.stringify(m, null, 2));
1516
1516
  '
1517
1517
  bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/swapped.motion.json \
1518
- --out /tmp/swapped --profile spine-html
1518
+ --out /tmp/swapped --profile rig-play
1519
1519
 
1520
1520
  # (b) FOLD IT: evaluate the same closed form at 40°, past the 31.37° of §4.2,
1521
1521
  # so the two far columns swap order and the mesh turns inside out. Since
@@ -1527,23 +1527,23 @@ for (const k of d.keys) if (k.transform) k.transform.degrees = 40;
1527
1527
  await Bun.write("/tmp/folded.motion.json", JSON.stringify(m, null, 2));
1528
1528
  '
1529
1529
  bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/folded.motion.json \
1530
- --out /tmp/folded --profile spine-html
1530
+ --out /tmp/folded --profile rig-play
1531
1531
  ```
1532
1532
 
1533
1533
  **What comes back from both:**
1534
1534
 
1535
1535
  | | good | (a) one band inverted | (b) mesh folded |
1536
1536
  | --- | --- | --- | --- |
1537
- | `--profile spine-html`, **without `A39`** | green | **green, and the same counts** | **green, and the same counts** |
1537
+ | `--profile rig-play`, **without `A39`** | green | **green, and the same counts** | **green, and the same counts** |
1538
1538
  | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | PASS | **PASS** | **PASS** |
1539
1539
  | the `MESH` coverage line | 100.00%, 95.90px past | **byte-identical** | **byte-identical** |
1540
1540
  | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | PASS | PASS | **FAIL, both keys, 8 of 32 triangles** |
1541
- | `--profile spine-html`, **with `A39`** | green | green | **refused, and nothing written** |
1541
+ | `--profile rig-play`, **with `A39`** | green | green | **refused, and nothing written** |
1542
1542
  | the `DEFORM` block, `head` key 1 `area` | x0.637174 … x1.319122 | **x0.765250 … x1.362834** | **x−0.288121 … x1.820211** |
1543
1543
  | … and its `winding` | 32 of 32 kept | 32 of 32 kept | **24 of 32 kept** |
1544
1544
 
1545
1545
  🚨 **All three were green, and the coverage line is the same string in all three,
1546
- because it reports the SETUP pose.** The `--profile spine-html` row with `A39` is
1546
+ because it reports the SETUP pose.** The `--profile rig-play` row with `A39` is
1547
1547
  the only verdict that separates them, and the two rows above it measure only the
1548
1548
  setup geometry: `A35` is silent about build (b). `A35` checks that a deform run *fits* its
1549
1549
  attachment — an honest and useful check, and orthogonal to whether the numbers in
@@ -1609,7 +1609,7 @@ bun cli.ts explain --rig gallery/portrait/rig.json \
1609
1609
  legitimate counter-example is the fold, which is `A39`'s.
1610
1610
 
1611
1611
  **`A39_DEFORM_KEEPS_TRIANGLE_WINDING` closes the half of that gap the fold
1612
- lives in** — `--profile spine-html`. Build (b) above is refused
1612
+ lives in** — `--profile rig-play`. Build (b) above is refused
1613
1613
  by name, on both its keys, with the triangles listed:
1614
1614
 
1615
1615
  ```
@@ -1711,7 +1711,7 @@ bun cli.ts check --candidate /tmp/folded --frames gallery/portrait/re
1711
1711
  ```
1712
1712
 
1713
1713
  ⚠️ **The third of those needs a build §9.2's own command will not write.** Build (b)
1714
- under `--profile spine-html` is refused and nothing
1714
+ under `--profile rig-play` is refused and nothing
1715
1715
  lands in `/tmp/folded` — which is the row above it in §9.2's table, working. Take
1716
1716
  that candidate from the **default** profile, where `A39` reads `PROF` and the
1717
1717
  artifact is written — a profile selects which assertions apply and not what is
@@ -1840,7 +1840,7 @@ it come off:
1840
1840
 
1841
1841
  `spine` is the default, so that is the `build` above with no `--profile` on it —
1842
1842
  the report names what the profile leaves out rather than this page counting it.
1843
- §9.2 runs the same rig under `--profile spine-html` and quotes what comes back
1843
+ §9.2 runs the same rig under `--profile rig-play` and quotes what comes back
1844
1844
  there; `A13_MESH_BUDGET` and `A15_IDLE_NO_MESH_BONE_KEYS` are among the rules
1845
1845
  the default profile excludes.
1846
1846
 
package/docs/INGEST.md CHANGED
@@ -1000,12 +1000,12 @@ other one is this project's own renderer and archetype policy.
1000
1000
  | **archetype policy** (8) | `A21_MESH_RIM_PINNED`, `A24_AXIS_SPACE_STROKE`, `A25_DETACHED_BONE_PARENTAGE`, `A26_SLOT_DRAW_ORDER`, `A28_RIBBON_ROWS_SHARE_WEIGHTS`, `A29_STROKE_WITHIN_CONTACT_DEPTH`, `A30_STROKE_WITHIN_CAP_CONTAINMENT`, `A39_DEFORM_KEEPS_TRIANGLE_WINDING` |
1001
1001
 
1002
1002
  Two further rules — **`A06`** and **`A20`** — are *mixed*: their validity clauses run
1003
- in both profiles and their policy clauses only under `spine-html`. `A06`'s
1003
+ in both profiles and their policy clauses only under `rig-play`. `A06`'s
1004
1004
  size-vs-PNG check is validity; rotation and premultiplied alpha are policy (two
1005
1005
  regions over the same texels was a policy clause of `A06` until it became `A49`). `A20`'s weight coherence is validity; requiring a mesh to be
1006
1006
  weighted at all is policy.
1007
1007
 
1008
- ⚠️ **`--profile spine-html` on foreign data produces a wall of failures that mean
1008
+ ⚠️ **`--profile rig-play` on foreign data produces a wall of failures that mean
1009
1009
  nothing about the file.** Same `spineboy-pro.json`, same atlas, one flag changed — the
1010
1010
  run ends `rigc: 13 assertion(s) failed`, and this is the tally with one real message
1011
1011
  per rule — `A06` takes a page that is one part covering it exactly *or a tiling of
@@ -1019,7 +1019,7 @@ regions*, so a packed atlas passes both profiles and the wall is policy only:
1019
1019
 
1020
1020
  Every one of those is a correct statement about a correct file: a bone *does* key a
1021
1021
  mesh, a mesh *is* unweighted, a clipping attachment *is* present.
1022
- ⇒ **Do not run `spine-html` against
1022
+ ⇒ **Do not run `rig-play` against
1023
1023
  somebody's export unless they asked whether it satisfies this project's renderer
1024
1024
  policy**, which is a different question from whether their file is valid.
1025
1025
 
@@ -1264,7 +1264,7 @@ something is invisible to every measure in that report. Pair it with a `check` a
1264
1264
  frames rendered from the original.
1265
1265
 
1266
1266
  ⚠️ **Do not rename toward what a rule seems to want.** `A27`'s
1267
- region-name-matches-page-filename is `spine-html` policy (§3.3): under the default
1267
+ region-name-matches-page-filename is `rig-play` policy (§3.3): under the default
1268
1268
  profile it does not fire, and renaming somebody's attachments to satisfy a policy they
1269
1269
  never opted into is a change with no benefit to them.
1270
1270
 
package/docs/RIGGING.md CHANGED
@@ -572,7 +572,7 @@ The same rig keys `headroll` and never `head`: *"a head rotates about the top of
572
572
  neck, not about the middle of its own face."* A renderer policy assertion
573
573
  (`A15_IDLE_NO_MESH_BONE_KEYS`, AUTHORING §5) forced that answer independently — two
574
574
  arguments, one bone. ⚠️ **It is a renderer rule, so like §10.3's `A25` it only fires
575
- under `--profile spine-html`**; under `--profile spine` it reports `PROF` and the
575
+ under `--profile rig-play`**; under `--profile spine` it reports `PROF` and the
576
576
  structural argument is the only one you get. [FACE.md](FACE.md) §3 is the same bone
577
577
  from the mesh's side.
578
578
 
@@ -1201,7 +1201,7 @@ Declare the pair in `invariants.detached` (AUTHORING §3.7) and it fires:
1201
1201
 
1202
1202
  ```bash
1203
1203
  rigc build --rig detached.rig.json --motion stack.motion.json --images parts \
1204
- --out det --profile spine-html
1204
+ --out det --profile rig-play
1205
1205
  ```
1206
1206
 
1207
1207
  ```
@@ -1414,7 +1414,7 @@ rigc build --rig stack.rig.json --motion stack.motion.json --images parts --out
1414
1414
  # .. pages=4 regions=4 bones=6 slots=5 animations=4 version=4.3.13 … profile=spine
1415
1415
  ```
1416
1416
 
1417
- ✅ It is green under **both** profiles, so `--profile spine-html` is available for
1417
+ ✅ It is green under **both** profiles, so `--profile rig-play` is available for
1418
1418
  §10.3 without any unrelated failure in the report.
1419
1419
 
1420
1420
  ### The three variants the sections build
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.49.0",
3
+ "version": "2.50.0",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,7 +37,7 @@ names the reversed triangles with their signed areas. `invariants.deformMayFold`
37
37
  in the rig spec is the one declared exemption, so reach for it when the slot
38
38
  folds on purpose and never to quiet a refusal you have not read. It is an
39
39
  **archetype** rule: a `--profile spine` build prints `PROF` for it and says
40
- nothing about the fold, so author under `--profile spine-html`.
40
+ nothing about the fold, so author under `--profile rig-play`.
41
41
 
42
42
  What `A39` still cannot say is whether the projection was the right one —
43
43
  nothing measures whether 12° was the angle the shot wanted. FACE §9.2 is that
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A06, the body (issue #1025, step 4c of #380): every page the atlas names is
3
3
  * the size its PNG is, every region lies inside its page, and — under
4
- * `spine-html` — no page claims premultiplied alpha and no region is turned.
4
+ * `rig-play` — no page claims premultiplied alpha and no region is turned.
5
5
  * The clause that refused two regions on one page overlapping is
6
6
  * `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` since issue #1099 (`./a49.ts`).
7
7
  *
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * A12, the body (issue #1025, cut 4c-4 of #380): no dark colour and no
3
- * two-colour timeline — both parsed, then silently ignored by spine-html.
3
+ * two-colour timeline — both parsed, then ignored by rig-play's DOM mode.
4
+ * Only the DOM mode is named (issue #1277): rig-play's canvas mode carries
5
+ * `dark` on the frame, but no test of the player's draws it, so nothing here
6
+ * says it is drawn there.
4
7
  *
5
8
  * Moved out of `src/validate.ts` unchanged but for what it reads: the slots
6
9
  * are a fact (`../facts/skeleton_roster.ts`) and the slot timelines are A45's
@@ -16,14 +19,14 @@ import type { SlotTimelines } from '../facts/slot_colour.ts';
16
19
  export function a12NoDarkColor({ fail }: Verdicts, { slots }: SkeletonRosterFacts, slotTimelines: readonly SlotTimelines[]): void {
17
20
  for (const slot of slots) {
18
21
  if (slot.dark) {
19
- fail('A12_NO_DARK_COLOR', `slot "${slot.name}" declares a dark colour; the renderer ignores it`);
22
+ fail('A12_NO_DARK_COLOR', `slot "${slot.name}" declares a dark colour; rig-play's DOM mode ignores it`);
20
23
  }
21
24
  }
22
25
  for (const { animation, slot, timelines } of slotTimelines) {
23
26
  for (const [name, keys] of Object.entries(timelines)) {
24
27
  if (!Array.isArray(keys)) continue;
25
28
  if (name === 'rgba2' || name === 'rgb2') {
26
- fail('A12_NO_DARK_COLOR', `${animation}.slots.${slot}.${name}: two-colour timeline "${name}" is silently ignored by the renderer`);
29
+ fail('A12_NO_DARK_COLOR', `${animation}.slots.${slot}.${name}: two-colour timeline "${name}" is ignored by rig-play's DOM mode`);
27
30
  }
28
31
  }
29
32
  }
@@ -145,7 +145,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
145
145
  // ⭐ Who names it is read off the two bodies, not chosen. Both ask
146
146
  // `readPngHeader` (`pngProblem`, the one reader of a page's identity) the
147
147
  // same question about the same file, A06 runs under both profiles and this
148
- // rule under `spine-html` alone, and A06 FAILs on every page for which it
148
+ // rule under `rig-play` alone, and A06 FAILs on every page for which it
149
149
  // answers with a problem — so on every input that reaches this branch, A06
150
150
  // has already named the file and the report is red. A FAIL here would be
151
151
  // the second naming and a PASS a certificate over alpha nobody read, so
@@ -246,7 +246,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
246
246
  `${region.y} is not where "${region.name}"'s texels are on this file and this rule states ` +
247
247
  'nothing about whether it can draw a transparent pixel. The page grid is ' +
248
248
  "A06_ATLAS_PAGE_SIZE_MATCHES_PNG's to judge, and it names the ratio and how to declare the page " +
249
- 'honestly. This is renderer policy, and it belongs to --profile spine-html: the default ' +
249
+ 'honestly. This is renderer policy, and it belongs to --profile rig-play: the default ' +
250
250
  '--profile spine does not run this check.',
251
251
  );
252
252
  }
@@ -280,7 +280,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
280
280
  `${plate.width}x${plate.height}, so it states nothing about whether "${region.name}" can draw a ` +
281
281
  "transparent pixel. A region's rectangle is A06_ATLAS_PAGE_SIZE_MATCHES_PNG's to judge, and one " +
282
282
  'that runs off its page is refused there by name. This is renderer policy, and it belongs to ' +
283
- '--profile spine-html: the default --profile spine does not run this check.',
283
+ '--profile rig-play: the default --profile spine does not run this check.',
284
284
  );
285
285
  continue;
286
286
  }
@@ -298,7 +298,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
298
298
  'A19_OVERLAY_PNGS_HAVE_ALPHA',
299
299
  `part "${region.name}" is opaque in ${over}, so it would paint a solid rectangle over whatever is ` +
300
300
  `drawn behind it. Re-export the part with transparency and pack again. ${exemption} This is ` +
301
- 'renderer policy, and it belongs to --profile spine-html: the default --profile spine does not run ' +
301
+ 'renderer policy, and it belongs to --profile rig-play: the default --profile spine does not run ' +
302
302
  'this check.',
303
303
  );
304
304
  }
@@ -316,7 +316,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
316
316
  `(${colourTypeName(info.colourType)}) with no tRNS chunk, so it would paint a solid rectangle over ` +
317
317
  'whatever is drawn behind it. Re-export it with transparency — as RGBA, or as an indexed or greyscale ' +
318
318
  `PNG that keeps its tRNS chunk. ${exemption} This is renderer policy, and it belongs to --profile ` +
319
- 'spine-html: the default --profile spine does not run this check.',
319
+ 'rig-play: the default --profile spine does not run this check.',
320
320
  );
321
321
  continue;
322
322
  }
@@ -352,7 +352,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
352
352
  `part image "${page.name}" is opaque in every one of its ${plate.width}x${plate.height} texels, so it ` +
353
353
  `would paint a solid rectangle over whatever is drawn behind it: its file can hold transparency — ${holds} ` +
354
354
  '— and no texel uses it. Re-export the part with the transparency it is meant to have. ' +
355
- `${exemption} This is renderer policy, and it belongs to --profile spine-html: the default --profile ` +
355
+ `${exemption} This is renderer policy, and it belongs to --profile rig-play: the default --profile ` +
356
356
  'spine does not run this check.',
357
357
  );
358
358
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A20, the body (issue #1025, step 4c of #380): a weighted mesh's weights
3
3
  * cohere — every vertex bound, every weight finite and non-negative, every
4
- * vertex's weights summing to 1 — and, under `spine-html`, a generated mesh is
4
+ * vertex's weights summing to 1 — and, under `rig-play`, a generated mesh is
5
5
  * weighted, binds only bones that move it and binds every bone it declares.
6
6
  *
7
7
  * Moved out of `src/validate.ts` clause by clause, every clause about the
@@ -72,7 +72,7 @@ export function a20MeshWeightsCoherent({ fail, skip }: Verdicts, { meshes, bones
72
72
  // In a rigc-GENERATED ring or ribbon it is still a defect: the
73
73
  // generator bound a bone that does nothing, which is a bug in the
74
74
  // generator and dead work in the runtime's inner loop. So it stays a
75
- // failure under spine-html and is not one under spine.
75
+ // failure under rig-play and is not one under spine.
76
76
  else if (policy && generated && weight === 0) {
77
77
  fail(
78
78
  'A20_MESH_WEIGHTS_COHERENT',
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Links nothing from the runtime.
24
24
  */
25
- import { ASSERTION_KIND, type AssertionProfile } from '../kinds.ts';
25
+ import { ASSERTION_KIND, resolveProfile, type AssertionProfile } from '../kinds.ts';
26
26
  import { verdictHarness, type VerdictLists } from '../harness.ts';
27
27
  import { MOVED_ASSERTIONS } from '../model/index.ts';
28
28
  import type { Verdicts } from '../harness.ts';
@@ -125,7 +125,9 @@ export function roundTripOnlyCodes(): string[] {
125
125
  }
126
126
 
127
127
  /** The restated rules over one build's emitted texts, in `validate()`'s report shape. */
128
- export function validateEmittedText(input: EmittedTextInput): VerdictLists & { profile: AssertionProfile } {
128
+ export function validateEmittedText(given: EmittedTextInput): VerdictLists & { profile: AssertionProfile } {
129
+ // A former profile name reads as its profile (issue #1277), for the rules and the report alike.
130
+ const input: EmittedTextInput = { ...given, profile: resolveProfile(given.profile, 'the emitted text') };
129
131
  const known = new Set(EMITTED_TEXT_RULES.map((r) => r.code));
130
132
  const unrun = roundTripOnlyCodes().filter((code) => !known.has(code));
131
133
  if (unrun.length > 0) {
@@ -18,7 +18,7 @@
18
18
  * This module links nothing from the runtime, directly or through what it
19
19
  * imports — the condition for the model side to use it, held by the selftest.
20
20
  */
21
- import { kindRunsUnder, type AssertionKind, type AssertionProfile } from './kinds.ts';
21
+ import { kindRunsUnder, resolveProfile, type AssertionKind, type AssertionProfile } from './kinds.ts';
22
22
 
23
23
  /** One failure: the assertion's code and the sentence naming the object, the value found and the value required. */
24
24
  export interface Failure {
@@ -75,6 +75,10 @@ export interface VerdictHarness extends VerdictLists {
75
75
  * the table lacks — `validate` for the gate, so its message is the one it was.
76
76
  */
77
77
  export function verdictHarness(profile: AssertionProfile, kinds: Readonly<Record<string, AssertionKind>>, who: string): VerdictHarness {
78
+ // A former name resolves to its profile and anything outside the pair is
79
+ // refused by name — `resolveProfile` says why. The entries resolve before
80
+ // they get here, so this is the floor for a caller of the harness itself.
81
+ profile = resolveProfile(profile, who);
78
82
  const failures: Failure[] = [];
79
83
  const passed: string[] = [];
80
84
  const skipped: VerdictLists['skipped'] = [];
@@ -12,7 +12,32 @@
12
12
  */
13
13
 
14
14
  /** The two rulebooks — `ValidateProfile` in `src/validate.ts`, where the choice between them is argued. */
15
- export type AssertionProfile = 'spine' | 'spine-html';
15
+ export type AssertionProfile = 'spine' | 'rig-play';
16
+
17
+ /**
18
+ * A profile's former name, which the CLI's `--profile` still reads for one
19
+ * minor and resolves at parse time, printing the deprecation line that names
20
+ * the profile it reads as (issue #1277). Nothing past the flag parser sees an
21
+ * alias: the rule set is keyed on the profile it resolves to, so the alias and
22
+ * its target cannot come to run different rules.
23
+ */
24
+ export const PROFILE_ALIASES: Readonly<Record<string, AssertionProfile>> = { 'spine-html': 'rig-play' };
25
+
26
+ /**
27
+ * The profile a gate entry runs under: a profile as given, a former name as the
28
+ * profile it became — silently, because the CLI prints the deprecation line and
29
+ * a library caller of `validate()` through a courtesy key passed the former
30
+ * name on the release before this one and must get the same rules — and
31
+ * anything else refused by name. `kindRunsUnder` reads every name but
32
+ * `rig-play` as validity-only, so an unresolved name would run a weaker gate in
33
+ * silence, under a name nobody chose (issue #1277).
34
+ */
35
+ export function resolveProfile(profile: string, who: string): AssertionProfile {
36
+ if (profile === 'spine' || profile === 'rig-play') return profile;
37
+ const target = Object.hasOwn(PROFILE_ALIASES, profile) ? PROFILE_ALIASES[profile] : undefined;
38
+ if (target !== undefined) return target;
39
+ throw new Error(`${who}: profile ${JSON.stringify(profile)} is not a profile; the profiles are "spine" and "rig-play"`);
40
+ }
16
41
 
17
42
  /** The three kinds a rule can be; the table's own comment says what each means. */
18
43
  export type AssertionKind = 'validity' | 'renderer' | 'archetype';
@@ -100,7 +125,7 @@ export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
100
125
  A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS: 'validity',
101
126
  };
102
127
 
103
- /** Whether `profile` runs a rule of `kind`: `spine` runs validity alone, `spine-html` every kind. */
128
+ /** Whether `profile` runs a rule of `kind`: `spine` runs validity alone, `rig-play` every kind. */
104
129
  export function kindRunsUnder(kind: AssertionKind, profile: AssertionProfile): boolean {
105
- return kind === 'validity' || profile === 'spine-html';
130
+ return kind === 'validity' || profile === 'rig-play';
106
131
  }
@@ -37,7 +37,7 @@
37
37
  * Links nothing from the runtime, directly or through what it imports — held
38
38
  * by the selftest's `VF` controls.
39
39
  */
40
- import { ASSERTION_KIND, type AssertionProfile } from '../kinds.ts';
40
+ import { ASSERTION_KIND, resolveProfile, type AssertionProfile } from '../kinds.ts';
41
41
  import { verdictHarness, type VerdictHarness, type Verdicts, type VerdictLists } from '../harness.ts';
42
42
  import { SKIP_NO_ATLAS, SKIP_NO_MODEL } from '../reasons.ts';
43
43
  import { a03RegionWidthHeightFinite } from '../bodies/a03.ts';
@@ -294,7 +294,7 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
294
294
  { code: 'A13_MESH_BUDGET', run: (v, read, input, supply) => a13MeshBudget(v, supply.meshes(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL },
295
295
  { code: 'A14_NO_FULL_FRAME_MESH', run: (v, read, input, supply) => a14NoFullFrameMesh(v, supply.meshes(read), supply.stage(read, input)), unread: SKIP_NO_MODEL },
296
296
  { code: 'A15_IDLE_NO_MESH_BONE_KEYS', run: (v, read, input, supply) => a15IdleNoMeshBoneKeys(v, supply.meshes(read), supply.animatedBones(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL },
297
- { code: 'A20_MESH_WEIGHTS_COHERENT', run: (v, read, input, supply) => a20MeshWeightsCoherent(v, supply.meshes(read), input.profile === 'spine-html', supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
297
+ { code: 'A20_MESH_WEIGHTS_COHERENT', run: (v, read, input, supply) => a20MeshWeightsCoherent(v, supply.meshes(read), input.profile === 'rig-play', supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
298
298
  { code: 'A21_MESH_RIM_PINNED', run: (v, read, input, supply) => a21MeshRimPinned(v, supply.meshes(read), supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
299
299
  { code: 'A22_MESH_UVS_IN_UNIT_RANGE', run: (v, read, _input, supply) => a22MeshUvsInUnitRange(v, supply.meshes(read)), unread: SKIP_NO_MODEL },
300
300
  { code: 'A39_DEFORM_KEEPS_TRIANGLE_WINDING', run: (v, read, input, supply) => a39DeformKeepsTriangleWinding(v, supply.deformSurvey(read), supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
@@ -313,7 +313,7 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
313
313
  { code: 'A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN', run: (v, read, _input, supply) => a44LinkedMeshStatesNoGeometryOfItsOwn(v, supply.links(read)), unread: SKIP_NO_MODEL },
314
314
  { code: 'A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES', run: (v, read, _input, supply) => a46SequenceAttachmentsShowTheFrameTheFileStates(v, supply.sequences(read)), unread: SKIP_NO_MODEL },
315
315
  { code: 'A17_ATLAS_PAGE_FILES_EXIST', run: (v, read, input, supply) => a17AtlasPageFilesExist(v, supply.atlasPages(read), input), unread: SKIP_NO_ATLAS },
316
- { code: 'A06_ATLAS_PAGE_SIZE_MATCHES_PNG', run: (v, read, input, supply) => a06AtlasPageSizeMatchesPng(v, supply.atlasRegions(read, input), input, input.profile === 'spine-html'), unread: SKIP_NO_ATLAS },
316
+ { code: 'A06_ATLAS_PAGE_SIZE_MATCHES_PNG', run: (v, read, input, supply) => a06AtlasPageSizeMatchesPng(v, supply.atlasRegions(read, input), input, input.profile === 'rig-play'), unread: SKIP_NO_ATLAS },
317
317
  { code: 'A19_OVERLAY_PNGS_HAVE_ALPHA', run: (v, read, input, supply) => a19OverlayPngsHaveAlpha(v, supply.atlasRegions(read, input), supply.stage(read, input), supply.skinEntries(read), { atlasDir: input.atlasDir, rig: supply.rigInfo(read, input) }), unread: SKIP_NO_ATLAS },
318
318
  { code: 'A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP', run: (v, read, input, supply) => a49PackedFootprintsDoNotOverlap(v, supply.atlasRegions(read, input), supply.regionJoins(read), supply.meshes(read)), unread: SKIP_NO_ATLAS },
319
319
  { code: 'A50_STAGE_BOX_IS_THE_STAGE', run: (v, read, _input, supply) => a50StageBoxIsTheStage(v, supply.stageBox(read)), unread: SKIP_NO_MODEL },
@@ -389,7 +389,9 @@ function rigFigures(stats: Record<string, number | string>, read: ReadDocument |
389
389
  * prints. `plant` replaces a family's supplier; only the selftest's plants
390
390
  * pass one.
391
391
  */
392
- export function validateModel(input: ModelValidateInput, plant: Partial<ModelSupply> = {}): ModelReport {
392
+ export function validateModel(given: ModelValidateInput, plant: Partial<ModelSupply> = {}): ModelReport {
393
+ // A former profile name reads as its profile (issue #1277), for the policy switches and the report alike.
394
+ const input: ModelValidateInput = { ...given, profile: resolveProfile(given.profile, 'the model side') };
393
395
  const supply: ModelSupply = { ...MODEL_SUPPLY, ...plant };
394
396
  const h = verdictHarness(input.profile, { ...ASSERTION_KIND, ...MODEL_PARSE_KIND }, 'the model side');
395
397
  const { read, held } = parse(h, input.modelText);
@@ -10,7 +10,18 @@ import type { AssertionProfile } from './kinds.ts';
10
10
  import type { VerdictLists } from './harness.ts';
11
11
  import { failLine } from '../guide.ts';
12
12
 
13
- export const VALIDATE_PROFILES: readonly AssertionProfile[] = ['spine', 'spine-html'];
13
+ export const VALIDATE_PROFILES: readonly AssertionProfile[] = ['spine', 'rig-play'];
14
+
15
+ /**
16
+ * The line `--profile <alias>` prints on stderr before the command runs: the
17
+ * old name, the profile it reads as, and the flag to pass instead (issue
18
+ * #1277). Stderr so a report on stdout reads the same whichever name the
19
+ * caller typed; the report itself names the profile that judged it, which is
20
+ * the target and never the alias.
21
+ */
22
+ export function profileAliasLine(alias: string, target: AssertionProfile): string {
23
+ return `rigc: --profile ${alias} is deprecated and reads as --profile ${target}, the same rules; it is accepted until the next minor release — pass --profile ${target}`;
24
+ }
14
25
 
15
26
  /**
16
27
  * What the CLI uses when `--profile` is absent — and ONLY the CLI. This is not
@@ -21,7 +32,7 @@ export const VALIDATE_PROFILES: readonly AssertionProfile[] = ['spine', 'spine-h
21
32
  * into the Spine editor", which is exactly the question `spine` asks, and a
22
33
  * stranger's first build was being judged instead against one renderer's policy
23
34
  * and one project's canvas budget — 14 rules they have no stake in, with the
24
- * escape hatch documented only in prose. Defaults beat prose. `spine-html` is
35
+ * escape hatch documented only in prose. Defaults beat prose. `rig-play` is
25
36
  * still one flag away, and every report names the profile that judged it.
26
37
  */
27
38
  export const CLI_DEFAULT_PROFILE: AssertionProfile = 'spine';
package/src/atlas.ts CHANGED
@@ -553,7 +553,7 @@ export interface EmitPage {
553
553
  * against the unturned pack. But on neither rig did a turn shrink the page: both
554
554
  * were already on the smallest power-of-two page their area allows, and a turn
555
555
  * that buys no area only moves bytes. Shipping it would also need two clauses
556
- * changed that say rigc never turns a region — `A06`'s under `spine-html`
556
+ * changed that say rigc never turns a region — `A06`'s under `rig-play`
557
557
  * ([`src/validate.ts`](validate.ts)) and `artUvsOf`
558
558
  * ([`src/render.ts`](render.ts)), which returns no art UVs for a turned region
559
559
  * attachment, so `check`'s texture substitution would report it unmatched.
package/src/cli/shared.ts CHANGED
@@ -26,10 +26,11 @@ import {
26
26
  CLI_DEFAULT_PROFILE,
27
27
  type GateHere,
28
28
  type PackPageFigures,
29
+ profileAliasLine,
29
30
  VALIDATE_PROFILES,
30
31
  } from '../assertions/report.ts';
31
32
  import type { VerdictLists } from '../assertions/harness.ts';
32
- import type { AssertionProfile } from '../assertions/kinds.ts';
33
+ import { PROFILE_ALIASES, type AssertionProfile } from '../assertions/kinds.ts';
33
34
  import { MAX_CANDIDATES, MIN_CANDIDATES } from '../ballot.ts';
34
35
  import { BONEDIST_SPEC, IDENTITY_CORRESPONDENCE } from '../correspondence.ts';
35
36
  import { ANCHOR_MAX_RESIDUAL, ANCHOR_MAX_UNEXPLAINED, DEFAULT_HINGE_MAX, DEFAULT_HINGE_MIN, DEFAULT_MIN_LEVER_PX, DEFAULT_MIN_VISIBLE, DEFAULT_PASSES } from '../chainfit.ts';
@@ -988,7 +989,7 @@ const FLAG_MEANINGS: Record<string, string> = {
988
989
  cuts: 'the cuts.json --cut names',
989
990
  profile:
990
991
  'which rulebook to check against (default: spine) — spine = valid Spine 4.3 that any runtime plays ' +
991
- "correctly; spine-html = also this project's renderer/archetype policy",
992
+ "correctly; rig-play = also this project's renderer/archetype policy",
992
993
  atlas: "the candidate's atlas, when it is not beside the skeleton",
993
994
  reference: 'the reference skeleton to pose beside the candidate — a directory or a skeleton.json path',
994
995
  'reference-atlas': "the reference's atlas, when it is not beside the reference skeleton",
@@ -1179,7 +1180,7 @@ const FLAG_VALUES: Record<string, string> = {
1179
1180
  manifest: '<path>',
1180
1181
  cut: '<name>',
1181
1182
  cuts: '<path>',
1182
- profile: 'spine|spine-html',
1183
+ profile: 'spine|rig-play',
1183
1184
  atlas: '<path>',
1184
1185
  'atlas-in': '<file.atlas>',
1185
1186
  'page-size': '<px>',
@@ -1324,7 +1325,7 @@ export const COMMANDS: CommandDoc[] = [
1324
1325
  for: 'the gate round-trips every build through it before anything is written',
1325
1326
  core: {
1326
1327
  usage: [
1327
- 'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|spine-html] [--copy-images] [--report <file>] (the same files the round-tripped build writes; gated without spine-core — see build --help)',
1328
+ 'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|rig-play] [--copy-images] [--report <file>] (the same files the round-tripped build writes; gated without spine-core — see build --help)',
1328
1329
  `rigc build … --pack [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] (parts onto shared pages, written into --out)`,
1329
1330
  'rigc build … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made)',
1330
1331
  'rigc build --cut <name> --cuts <cuts.json>',
@@ -1348,7 +1349,7 @@ export const COMMANDS: CommandDoc[] = [
1348
1349
  },
1349
1350
  spineFormat: true,
1350
1351
  usage: [
1351
- 'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|spine-html] [--copy-images] [--report <file>]',
1352
+ 'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|rig-play] [--copy-images] [--report <file>]',
1352
1353
  `rigc build … --pack [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] (parts onto shared pages, written into --out)`,
1353
1354
  'rigc build … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made)',
1354
1355
  'rigc build --cut <name> --cuts <cuts.json>',
@@ -1378,8 +1379,8 @@ export const COMMANDS: CommandDoc[] = [
1378
1379
  for: 'the rebuild it writes is gated by build\'s gate, which round-trips it through spine-core',
1379
1380
  core: {
1380
1381
  usage: [
1381
- `rigc repack <dir | skeleton.json> --out <dir> [--atlas <path>] [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] [--profile spine|spine-html] [--stage x,y,w,h] [--stage-box <slot>] [--accept-skeleton-differences] [--report <file>] (gated without spine-core — see repack --help)`,
1382
- `rigc repack <dir | skeleton.json> --out <dir> --keep-pages [--atlas <path>] [--profile spine|spine-html] [--stage x,y,w,h] [--stage-box <slot>] [--report <file>] (skeleton.model.json alone, pages kept)`,
1382
+ `rigc repack <dir | skeleton.json> --out <dir> [--atlas <path>] [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] [--profile spine|rig-play] [--stage x,y,w,h] [--stage-box <slot>] [--accept-skeleton-differences] [--report <file>] (gated without spine-core — see repack --help)`,
1383
+ `rigc repack <dir | skeleton.json> --out <dir> --keep-pages [--atlas <path>] [--profile spine|rig-play] [--stage x,y,w,h] [--stage-box <slot>] [--report <file>] (skeleton.model.json alone, pages kept)`,
1383
1384
  ],
1384
1385
  notes: [
1385
1386
  'a packed build repacked from its own output — skeleton.json, skeleton.atlas and the',
@@ -1401,8 +1402,8 @@ export const COMMANDS: CommandDoc[] = [
1401
1402
  },
1402
1403
  spineFormat: true,
1403
1404
  usage: [
1404
- `rigc repack <dir | skeleton.json> --out <dir> [--atlas <path>] [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] [--profile spine|spine-html] [--stage x,y,w,h] [--stage-box <slot>] [--accept-skeleton-differences] [--report <file>]`,
1405
- `rigc repack <dir | skeleton.json> --out <dir> --keep-pages [--atlas <path>] [--profile spine|spine-html] [--stage x,y,w,h] [--stage-box <slot>] [--report <file>] (skeleton.model.json alone, pages kept)`,
1405
+ `rigc repack <dir | skeleton.json> --out <dir> [--atlas <path>] [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] [--profile spine|rig-play] [--stage x,y,w,h] [--stage-box <slot>] [--accept-skeleton-differences] [--report <file>]`,
1406
+ `rigc repack <dir | skeleton.json> --out <dir> --keep-pages [--atlas <path>] [--profile spine|rig-play] [--stage x,y,w,h] [--stage-box <slot>] [--report <file>] (skeleton.model.json alone, pages kept)`,
1406
1407
  ],
1407
1408
  flags: ['out', 'atlas', 'page-size', 'padding', 'page-edges', 'pack-shape', 'profile', 'stage', 'stage-box', 'accept-skeleton-differences', 'keep-pages', 'report'],
1408
1409
  overrides: {
@@ -1469,7 +1470,7 @@ export const COMMANDS: CommandDoc[] = [
1469
1470
  runtime: { for: 'the gate it re-runs is the round trip through it', rerunsTheGate: true },
1470
1471
  spineFormat: true,
1471
1472
  usage: [
1472
- 'rigc validate <dir | skeleton.json> [--atlas <path>] [--profile spine|spine-html]',
1473
+ 'rigc validate <dir | skeleton.json> [--atlas <path>] [--profile spine|rig-play]',
1473
1474
  'rigc validate --cut <name> --cuts <cuts.json> (also re-derives declared durations)',
1474
1475
  ],
1475
1476
  flags: ['atlas', 'profile', 'cut', 'cuts', 'rig', 'motion', 'out', 'manifest', 'images'],
@@ -1815,17 +1816,17 @@ export function commandHelp(name: string, docs: readonly CommandDoc[] = COMMANDS
1815
1816
  * `--profile` paragraph states each profile's rule count, which only the
1816
1817
  * validator knows (`CliEntry.profileRules`).
1817
1818
  */
1818
- const USAGE_PARAGRAPHS: ReadonlyArray<{ about: readonly string[] | { flag: string }; lines: (rules: (profile: 'spine' | 'spine-html') => number) => string[] }> = [
1819
+ const USAGE_PARAGRAPHS: ReadonlyArray<{ about: readonly string[] | { flag: string }; lines: (rules: (profile: 'spine' | 'rig-play') => number) => string[] }> = [
1819
1820
  {
1820
1821
  about: ['build', 'validate', 'bench'],
1821
1822
  lines: (rules) => [
1822
- 'build, validate and bench take --profile spine|spine-html:',
1823
+ 'build, validate and bench take --profile spine|rig-play:',
1823
1824
  ' spine is this valid Spine 4.3 that any runtime plays correctly?',
1824
1825
  ` THE DEFAULT — ${rules('spine')} rules, and the question the output answers when`,
1825
1826
  ' you import it into the Spine editor.',
1826
- ' spine-html the above, plus this project\'s renderer and archetype policy:',
1827
- ` all ${rules('spine-html')} rules, opt-in. Those extra ` +
1828
- `${rules('spine-html') - rules('spine')} fire on real, correct,`,
1827
+ ' rig-play the above, plus this project\'s renderer and archetype policy:',
1828
+ ` all ${rules('rig-play')} rules, opt-in. Those extra ` +
1829
+ `${rules('rig-play') - rules('spine')} fire on real, correct,`,
1829
1830
  ' editor-produced Spine data, so they are somebody\'s policy rather',
1830
1831
  ' than anybody\'s validity.',
1831
1832
  '',
@@ -1955,7 +1956,7 @@ const USAGE_PARAGRAPHS: ReadonlyArray<{ about: readonly string[] | { flag: strin
1955
1956
  * lines, then every paragraph about only those commands (`USAGE_PARAGRAPHS`).
1956
1957
  * `checkout` is the file a source checkout runs this entry as.
1957
1958
  */
1958
- export function usageText(docs: readonly CommandDoc[], checkout: string, rules: ((profile: 'spine' | 'spine-html') => number) | undefined): string {
1959
+ export function usageText(docs: readonly CommandDoc[], checkout: string, rules: ((profile: 'spine' | 'rig-play') => number) | undefined): string {
1959
1960
  const names = new Set(docs.map((doc) => doc.name));
1960
1961
  const takes = (flag: string): boolean => docs.some((doc) => doc.flags.includes(flag));
1961
1962
  const lines = [
@@ -2002,6 +2003,13 @@ export function usageText(docs: readonly CommandDoc[], checkout: string, rules:
2002
2003
  export function readProfile(flags: Record<string, string>): AssertionProfile {
2003
2004
  const raw = flags.profile;
2004
2005
  if (raw === undefined) return CLI_DEFAULT_PROFILE;
2006
+ // A former name resolves here and nowhere else (issue #1277): past this line
2007
+ // the alias does not exist, so it cannot select a rule set of its own.
2008
+ const aliased = Object.hasOwn(PROFILE_ALIASES, raw) ? PROFILE_ALIASES[raw] : undefined;
2009
+ if (aliased !== undefined) {
2010
+ console.error(profileAliasLine(raw, aliased));
2011
+ return aliased;
2012
+ }
2005
2013
  const found = VALIDATE_PROFILES.find((p) => p === raw);
2006
2014
  if (!found) throw new UsageError(`--profile ${JSON.stringify(raw)}; known profiles: ${VALIDATE_PROFILES.join(', ')}`);
2007
2015
  return found;
@@ -2633,7 +2641,7 @@ export interface CliEntry {
2633
2641
  /** Every command this entry runs, by name — exactly its set, or the run refuses to start. */
2634
2642
  runs: Readonly<Record<string, CommandRun>>;
2635
2643
  /** Each profile's rule count, for the `--profile` paragraph; the validator's, so only an entry that links it states it. */
2636
- profileRules?: (profile: 'spine' | 'spine-html') => number;
2644
+ profileRules?: (profile: 'spine' | 'rig-play') => number;
2637
2645
  /** The refusals of bodies only this entry registers (`CliRefusal`). */
2638
2646
  refusals?: readonly CliRefusal[];
2639
2647
  }
@@ -818,4 +818,4 @@ export const SPINE_COMMAND_REFUSALS: readonly CliRefusal[] = [
818
818
  ];
819
819
 
820
820
  /** Each profile's rule count — the validator's, which the usage's `--profile` paragraph states. */
821
- export const PROFILE_RULES = (profile: 'spine' | 'spine-html'): number => assertionCountForProfile(profile);
821
+ export const PROFILE_RULES = (profile: 'spine' | 'rig-play'): number => assertionCountForProfile(profile);
package/src/compile.ts CHANGED
@@ -1420,7 +1420,7 @@ const SLIDER_TRACKS: Record<string, ValueTrackShape> = {
1420
1420
  * refused by `A12_NO_DARK_COLOR`, and that is not a contradiction — A12 is filed
1421
1421
  * `renderer` in `ASSERTION_KIND`, so it states what ONE renderer ignores rather
1422
1422
  * than what is wrong. The `spine` profile `build` runs reports it `PROF` and
1423
- * never applies it; `--profile spine-html` is where it fires. Emitting a
1423
+ * never applies it; `--profile rig-play` is where it fires. Emitting a
1424
1424
  * construct one consumer drops is exactly what a profile is for.
1425
1425
  */
1426
1426
  export const SLOT_TRACKS: Record<string, 'attachment' | SlotColourShape> = {
package/src/rig.ts CHANGED
@@ -426,7 +426,7 @@ export interface RigSlot {
426
426
  attachment?: string | null;
427
427
  /** `rrggbbaa`. Default opaque white. */
428
428
  color?: string;
429
- /** Two-colour tint, `rrggbb`. 🚫 `A12_NO_DARK_COLOR` under `spine-html`. */
429
+ /** Two-colour tint, `rrggbb`. 🚫 `A12_NO_DARK_COLOR` under `rig-play`. */
430
430
  dark?: string;
431
431
  /** Default `normal`. Only the first letter's case is free (`RigSlotBlend`). */
432
432
  blend?: RigSlotBlend;
package/src/validate.ts CHANGED
@@ -77,7 +77,7 @@ import {
77
77
  walkTimelines,
78
78
  } from './timelines.ts';
79
79
  import type { RigInfo } from './types.ts';
80
- import { ASSERTION_KIND, kindRunsUnder, type AssertionProfile } from './assertions/kinds.ts';
80
+ import { ASSERTION_KIND, kindRunsUnder, resolveProfile, type AssertionProfile } from './assertions/kinds.ts';
81
81
  import { verdictHarness, type Failure } from './assertions/harness.ts';
82
82
  import { isObj, type Json } from './assertions/values.ts';
83
83
  import type { SkinEntryFacts } from './assertions/facts/skin_entries.ts';
@@ -168,14 +168,14 @@ export type { Failure } from './assertions/harness.ts';
168
168
  * ⭐ The distinction this draws is the difference between "wrong" and "not how we
169
169
  * do it here", and conflating the two is how a validator stops being usable on
170
170
  * anybody else's data. Fifteen of the 40 assertions are policy — seven for one
171
- * renderer (`spine-html`) and one project's canvas budget, eight for rigc's own
171
+ * renderer (`rig-play`) and one project's canvas budget, eight for rigc's own
172
172
  * formations — and every one of them fires
173
173
  * on real, correct, editor-produced Spine data — the official example projects
174
174
  * carry clipping attachments, unweighted meshes, 116-triangle meshes and packed
175
175
  * atlases, all of which are perfectly valid and none of which spine-html likes.
176
176
  *
177
177
  * - `spine` — is this valid Spine 4.3 that any runtime will play correctly?
178
- * - `spine-html` — the above, plus this project's renderer and archetype policy.
178
+ * - `rig-play` — the above, plus this project's renderer and archetype policy.
179
179
  *
180
180
  * ⚠️ `validate()` has NO default profile — `ValidateInput.profile` is required,
181
181
  * and that is deliberate. A silent default here can only be wrong in one of two
@@ -207,7 +207,7 @@ export const ASSERTION_NAMES: readonly string[] = Object.keys(ASSERTION_KIND);
207
207
 
208
208
  /** How many assertions this profile applies, by the kinds it carries. */
209
209
  export function assertionCountForProfile(profile: ValidateProfile): number {
210
- if (profile === 'spine-html') return ASSERTION_NAMES.length;
210
+ if (profile === 'rig-play') return ASSERTION_NAMES.length;
211
211
  return ASSERTION_NAMES.filter((name) => ASSERTION_KIND[name] === 'validity').length;
212
212
  }
213
213
 
@@ -2012,9 +2012,10 @@ function once<T>(supply: () => T): () => T {
2012
2012
  }
2013
2013
 
2014
2014
  export function validate(input: ValidateInput): ValidateReport {
2015
- const profile = input.profile;
2015
+ // A former name reads as its profile (issue #1277): the policy switch below and the report both use the resolved one.
2016
+ const profile = resolveProfile(input.profile, 'validate');
2016
2017
  /** True when this profile's rulebook includes the policy layer. */
2017
- const policy = profile === 'spine-html';
2018
+ const policy = profile === 'rig-play';
2018
2019
  // The harness — `check`, `fail`, `skip` and the lists they fill — is
2019
2020
  // `./assertions/harness.ts`'s since issue #1025, and the rule is the one it
2020
2021
  // always was: the model side runs the bodies that moved there in the same
@@ -2566,7 +2567,7 @@ export function validate(input: ValidateInput): ValidateReport {
2566
2567
  });
2567
2568
 
2568
2569
  // --- A12: no dark / two-colour tint --------------------------------------
2569
- // Parsed, then silently ignored by spine-html.
2570
+ // Parsed, then ignored by rig-play's DOM mode.
2570
2571
  check('A12_NO_DARK_COLOR', () => a12NoDarkColor(verdicts, rawSkeletonRoster(raw), rawSlotTimelines(raw)));
2571
2572
 
2572
2573
  // --- A05: curve arrays are 4 numbers per value channel --------------------
@@ -3309,7 +3310,7 @@ export function validate(input: ValidateInput): ValidateReport {
3309
3310
  // first build — and told them, untruthfully, that the file held no transparency
3310
3311
  // at all. That audit is also why the CLI's default is now `spine`, which does
3311
3312
  // not run this rule at all (#221): a stranger reaches this refusal only by
3312
- // asking for `spine-html`. It still has to be both TRUE and actionable when it
3313
+ // asking for `rig-play`. It still has to be both TRUE and actionable when it
3313
3314
  // fires, and it now has to name the profile it belongs to as well as the one
3314
3315
  // that does not ask — the reader opted in, and the message is where they find
3315
3316
  // out what they opted into.