spine-rigc 2.49.0 → 2.51.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.
@@ -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
  }
@@ -25,7 +25,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
25
25
  // A SKIP for the same reason A06's is (#568). This one is invisible under
26
26
  // `spine`, where the profile excludes the rule before its body runs — and
27
27
  // that is exactly why it was worth finding: `--profile spine-html` reported
28
- // it, and A27 below, green on an atlas nothing had read.
28
+ // it, and A27 (retired since, issue #1404), green on an atlas nothing had read.
29
29
  if (!atlas) return skip('A19_OVERLAY_PNGS_HAVE_ALPHA', SKIP_NO_ATLAS);
30
30
  if (atlas.pages.length === 0) return skip('A19_OVERLAY_PNGS_HAVE_ALPHA', SKIP_NO_ATLAS_PAGE);
31
31
  // Every sentence this body prints goes through here, so its end can tell
@@ -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) {
@@ -2,10 +2,10 @@
2
2
  * The atlas as the page rules read it (issue #1025, step 4c of #380) — the
3
3
  * census's F20 and F21 whole: every page's name, declared size and `pma`, every
4
4
  * region's rectangle and the page it sits on, and the region a name resolves
5
- * to. A06, A19 and A27 read it; A17, which reads only the page names, keeps the
5
+ * to. A06, A19 and A49 read it; A17, which reads only the page names, keeps the
6
6
  * narrower `./atlas_pages.ts`.
7
7
  *
8
- * ⭐ **The order is a fact, and it is the file's.** A06 and A27 print one line
8
+ * ⭐ **The order is a fact, and it is the file's.** A06 prints one line
9
9
  * per offending page or region, and A06's overlap clause prints a pair in the
10
10
  * order it meets them, so `pages` and `regions` hold the atlas's own order —
11
11
  * pages as the file lists them, regions page by page in the file's order — and
@@ -28,10 +28,8 @@ export interface RegionEntry {
28
28
  readonly path?: string;
29
29
  }
30
30
 
31
- /** What the skins hold, for A03, A11 and A19. */
31
+ /** What the skins hold, for A03 and A19. */
32
32
  export interface SkinEntryFacts {
33
33
  /** Every region attachment of every skin, in the file's walk order (the header's ⭐). */
34
34
  readonly regionAttachments: readonly RegionEntry[];
35
- /** How many clipping attachments the skins hold, every skin counted. */
36
- readonly clippingCount: number;
37
35
  }
@@ -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';
@@ -45,6 +70,13 @@ export type AssertionKind = 'validity' | 'renderer' | 'archetype';
45
70
  * restating it as the renderer's own join made it a tautology over the validity
46
71
  * half. The argument, with the renderer lines it is measured against, sits above
47
72
  * `check('A08_…')`.
73
+ *
74
+ * The numbering has gaps, and they are retirements, not reservations: A41
75
+ * (issue #1202), and A11 and A27 (issue #1404) — two `renderer` rules stating
76
+ * limits the player `rig-play` names does not have, since it draws a clipping
77
+ * attachment and a region on a page named for something else in both of its
78
+ * modes. A retired code is not reused, so a line that names one in an old
79
+ * report still means what it meant.
48
80
  */
49
81
  export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
50
82
  A00_ROUNDTRIP_PARSE: 'validity',
@@ -58,7 +90,6 @@ export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
58
90
  A08_REGION_NAMES_MATCH_ATTACHMENTS: 'validity', // no longer mixed — see above (#574)
59
91
  A09_ANIMATION_DURATION_MATCHES_SPEC: 'validity',
60
92
  A10_NO_NAN_AFTER_STEPPING: 'validity',
61
- A11_NO_CLIPPING_ATTACHMENTS: 'renderer',
62
93
  A12_NO_DARK_COLOR: 'renderer',
63
94
  A13_MESH_BUDGET: 'renderer',
64
95
  A14_NO_FULL_FRAME_MESH: 'renderer',
@@ -74,7 +105,6 @@ export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
74
105
  A24_AXIS_SPACE_STROKE: 'archetype',
75
106
  A25_DETACHED_BONE_PARENTAGE: 'archetype',
76
107
  A26_SLOT_DRAW_ORDER: 'archetype',
77
- A27_REGION_NAME_MATCHES_PAGE_FILENAME: 'renderer',
78
108
  A28_RIBBON_ROWS_SHARE_WEIGHTS: 'archetype',
79
109
  A29_STROKE_WITHIN_CONTACT_DEPTH: 'archetype',
80
110
  A30_STROKE_WITHIN_CAP_CONTAINMENT: 'archetype',
@@ -100,7 +130,7 @@ export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
100
130
  A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS: 'validity',
101
131
  };
102
132
 
103
- /** Whether `profile` runs a rule of `kind`: `spine` runs validity alone, `spine-html` every kind. */
133
+ /** Whether `profile` runs a rule of `kind`: `spine` runs validity alone, `rig-play` every kind. */
104
134
  export function kindRunsUnder(kind: AssertionKind, profile: AssertionProfile): boolean {
105
- return kind === 'validity' || profile === 'spine-html';
135
+ return kind === 'validity' || profile === 'rig-play';
106
136
  }
@@ -37,11 +37,10 @@
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';
44
- import { a11NoClippingAttachments } from '../bodies/a11.ts';
45
44
  import { a17AtlasPageFilesExist } from '../bodies/a17.ts';
46
45
  import { a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten } from '../bodies/a45.ts';
47
46
  import { A00_MODEL_READ, A00_MODEL_REGIONS_ON_PAGES, MODEL_PARSE_KIND, modelRead, modelRegionsOnPages, SKIP_NO_MODEL_DOCUMENT, SKIP_NO_MODEL_PAGES, type ReadDocument } from './parse.ts';
@@ -69,7 +68,6 @@ import { modelSetupWorld } from './setup_world.ts';
69
68
  import type { SetupWorldFacts } from '../facts/setup_world.ts';
70
69
  import { modelStageBox } from './stage_box.ts';
71
70
  import type { StageBoxFacts } from '../facts/stage_box.ts';
72
- import { a27RegionNameMatchesPageFilename } from '../bodies/a27.ts';
73
71
  import { modelAnimatedBones } from './animated_bones.ts';
74
72
  import { modelSkinMembers } from './skin_members.ts';
75
73
  import { modelRegionJoins } from './region_joins.ts';
@@ -290,11 +288,10 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
290
288
  { code: 'A03_REGION_WIDTH_HEIGHT_FINITE', run: (v, read, _input, supply) => a03RegionWidthHeightFinite(v, supply.skinEntries(read)), unread: SKIP_NO_MODEL },
291
289
  { code: 'A04_MESH_TRIANGLES_AND_ENCODING', run: (v, read, _input, supply) => a04MeshTrianglesAndEncoding(v, supply.meshes(read)), unread: SKIP_NO_MODEL },
292
290
  { code: 'A33_VERTEX_ATTACHMENT_GEOMETRY', run: (v, read, _input, supply) => a33VertexAttachmentGeometry(v, supply.polygons(read)), unread: SKIP_NO_MODEL },
293
- { code: 'A11_NO_CLIPPING_ATTACHMENTS', run: (v, read, _input, supply) => a11NoClippingAttachments(v, supply.skinEntries(read)), unread: SKIP_NO_MODEL },
294
291
  { code: 'A13_MESH_BUDGET', run: (v, read, input, supply) => a13MeshBudget(v, supply.meshes(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL },
295
292
  { 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
293
  { 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 },
294
+ { 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
295
  { code: 'A21_MESH_RIM_PINNED', run: (v, read, input, supply) => a21MeshRimPinned(v, supply.meshes(read), supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
299
296
  { code: 'A22_MESH_UVS_IN_UNIT_RANGE', run: (v, read, _input, supply) => a22MeshUvsInUnitRange(v, supply.meshes(read)), unread: SKIP_NO_MODEL },
300
297
  { 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,14 +310,13 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
313
310
  { 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
311
  { 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
312
  { 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 },
313
+ { 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
314
  { 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
315
  { 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
316
  { code: 'A50_STAGE_BOX_IS_THE_STAGE', run: (v, read, _input, supply) => a50StageBoxIsTheStage(v, supply.stageBox(read)), unread: SKIP_NO_MODEL },
320
317
  { code: 'A24_AXIS_SPACE_STROKE', run: (v, read, input, supply) => a24AxisSpaceStroke(v, supply.boneTimelines(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
321
318
  { code: 'A25_DETACHED_BONE_PARENTAGE', run: (v, read, input, supply) => a25DetachedBoneParentage(v, supply.skeletonRoster(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
322
319
  { code: 'A26_SLOT_DRAW_ORDER', run: (v, read, input, supply) => a26SlotDrawOrder(v, supply.skeletonRoster(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
323
- { code: 'A27_REGION_NAME_MATCHES_PAGE_FILENAME', run: (v, read, input, supply) => a27RegionNameMatchesPageFilename(v, supply.atlasRegions(read, input)), unread: SKIP_NO_ATLAS },
324
320
  { code: 'A28_RIBBON_ROWS_SHARE_WEIGHTS', run: (v, read, input, supply) => a28RibbonRowsShareWeights(v, supply.meshes(read), supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
325
321
  { code: 'A29_STROKE_WITHIN_CONTACT_DEPTH', run: (v, read, input, supply) => a29StrokeWithinContactDepth(v, supply.boneTimelines(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
326
322
  { code: 'A30_STROKE_WITHIN_CAP_CONTAINMENT', run: (v, read, input, supply) => a30StrokeWithinCapContainment(v, supply.boneTimelines(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
@@ -389,7 +385,9 @@ function rigFigures(stats: Record<string, number | string>, read: ReadDocument |
389
385
  * prints. `plant` replaces a family's supplier; only the selftest's plants
390
386
  * pass one.
391
387
  */
392
- export function validateModel(input: ModelValidateInput, plant: Partial<ModelSupply> = {}): ModelReport {
388
+ export function validateModel(given: ModelValidateInput, plant: Partial<ModelSupply> = {}): ModelReport {
389
+ // A former profile name reads as its profile (issue #1277), for the policy switches and the report alike.
390
+ const input: ModelValidateInput = { ...given, profile: resolveProfile(given.profile, 'the model side') };
393
391
  const supply: ModelSupply = { ...MODEL_SUPPLY, ...plant };
394
392
  const h = verdictHarness(input.profile, { ...ASSERTION_KIND, ...MODEL_PARSE_KIND }, 'the model side');
395
393
  const { read, held } = parse(h, input.modelText);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The model side's supply of `SkinEntryFacts` (issue #1025, step 4c of #380):
3
- * the skins' region attachments and clipping count, read off the document.
3
+ * the skins' region attachments, read off the document.
4
4
  *
5
5
  * ⭐ **In the file's order, by the emitter's own rule.** The document holds the
6
6
  * skins in the spec's order and the Spine file holds them in the editor's
@@ -55,9 +55,7 @@ export function fileOrderedEntries(read: ReadDocument, order?: SkinOrder): Array
55
55
 
56
56
  export function modelSkinEntries(read: ReadDocument, order?: SkinOrder): SkinEntryFacts {
57
57
  const regionAttachments: RegionEntry[] = [];
58
- let clippingCount = 0;
59
58
  for (const entry of fileOrderedEntries(read, order)) {
60
- if (entry.record.kind === 'clipping') clippingCount++;
61
59
  if (entry.record.kind !== 'region') continue;
62
60
  const geometry = entry.record.geometry;
63
61
  // `readModel` reads every region record's geometry or refuses the document, so a record without one is this module's defect.
@@ -65,5 +63,5 @@ export function modelSkinEntries(read: ReadDocument, order?: SkinOrder): SkinEnt
65
63
  const shown = shownRow(entry);
66
64
  regionAttachments.push({ name: shown.name, width: geometry.region.width, height: geometry.region.height, path: shown.path as string });
67
65
  }
68
- return { regionAttachments, clippingCount };
66
+ return { regionAttachments };
69
67
  }
@@ -28,7 +28,7 @@ export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure
28
28
  /**
29
29
  * The third of them, and it is NOT waiting on A00 (issue #608): the atlas parsed
30
30
  * and it has no pages, which is what a compile that measured no art writes. The
31
- * four rules whose only subject is a page — A06, A17, A19, A27 — have nothing to
31
+ * three rules whose only subject is a page — A06, A17, A19 — have nothing to
32
32
  * look at, and an empty loop walking out of `check()` is reported as a PASS.
33
33
  */
34
34
  /*
@@ -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;