spine-rigc 0.34.0 → 0.35.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 +5 -5
- package/cli.ts +37 -6
- package/docs/AUTHORING.md +381 -43
- package/docs/INGEST.md +64 -16
- package/docs/SPEC_COVERAGE.md +1 -1
- package/package.json +1 -1
- package/src/compile.ts +73 -3
- package/src/ingest.ts +196 -26
- package/src/keyorder.ts +547 -0
- package/src/mesh.ts +54 -17
- package/src/rig.ts +119 -9
- package/src/types.ts +36 -17
- package/src/validate.ts +129 -12
package/src/ingest.ts
CHANGED
|
@@ -63,7 +63,8 @@ import {
|
|
|
63
63
|
TOPLEVEL_CONSTRAINT_ARRAYS,
|
|
64
64
|
} from './generation.ts';
|
|
65
65
|
import { EVERY_GLOBAL_PHYSICS, MOTION_SPEC_VERSION, parseMotionSpec } from './motion.ts';
|
|
66
|
-
import {
|
|
66
|
+
import { PARSER_DEFAULTS, parserOmits } from './keyorder.ts';
|
|
67
|
+
import { constraintAt, parseRigSpec, resolveBoneInherit, RIG_KEYS, RIG_SPEC_VERSION, type RigSpec } from './rig.ts';
|
|
67
68
|
import type { MotionSpec } from './types.ts';
|
|
68
69
|
|
|
69
70
|
/**
|
|
@@ -126,7 +127,7 @@ export class IngestSpecRefused extends Error {
|
|
|
126
127
|
* are exactly two: a stage the caller supplied, and an animation's duration.
|
|
127
128
|
* - `lossy` — the skeleton's spelling and rigc's differ, on purpose, and the
|
|
128
129
|
* difference is named: a value rigc re-derives rather than takes (`lengths`,
|
|
129
|
-
* the `spine` version), a field the spec has no home for (`hash
|
|
130
|
+
* the `spine` version), a field the spec has no home for (`hash`), or
|
|
130
131
|
* a default the source left to the format and the rebuild writes out
|
|
131
132
|
* (`HEADER_ORIGIN`, issue #622). The rebuilt file is a different file in that
|
|
132
133
|
* field; it is not a different rig.
|
|
@@ -331,6 +332,93 @@ function inertPhysics(root: JsonObject): Map<string, string[]> {
|
|
|
331
332
|
return out;
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
/**
|
|
336
|
+
* The two kinds `A47` / `A48` ask the muted-at-rest question of. An alias rather
|
|
337
|
+
* than the union written at each use, because `IG25`'s scan resolves a composed
|
|
338
|
+
* finding code against the nearest `<name>: '…' | '…'` above it, and `type:` is
|
|
339
|
+
* the identifier `ATTACHMENT_<TYPE>` composes from further down.
|
|
340
|
+
*/
|
|
341
|
+
type MixedConstraintKind = 'ik' | 'transform';
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The ik and transform constraints this file rests muted and never switches on
|
|
345
|
+
* — the shape `A47` / `A48` refuse — with the mixes each one reads (issue #784).
|
|
346
|
+
*
|
|
347
|
+
* 🔑 **Read the way the gate reads it, from the raw JSON.** This module does not
|
|
348
|
+
* link the runtime, so the defaults are the parser's own, spelled here: an ik
|
|
349
|
+
* `mix` is 1 when absent, on the constraint and on every key; a transform mix
|
|
350
|
+
* is 1 when absent except `mixY`, which is the same object's `mixX`, and
|
|
351
|
+
* `mixScaleY`, which is its `mixScaleX` (`SkeletonJson.js`, every
|
|
352
|
+
* `getValue(…, 1)`). A transform reads only the mixes of the `to` properties it
|
|
353
|
+
* declares, and one that declares none is `A48`'s other sentence, which no
|
|
354
|
+
* declaration answers — so it is not a candidate. Live is `!== 0`, the
|
|
355
|
+
* runtime's test. A key lifts a mix when the value it states is live, or when
|
|
356
|
+
* its Bezier handles for that channel are — the curve the runtime interpolates
|
|
357
|
+
* through (`curveChannelValues` in `validate.ts`), which is how a 0 → 0 pair
|
|
358
|
+
* with raised handles moves the bones.
|
|
359
|
+
*
|
|
360
|
+
* ⚠️ A disagreement with the gate is loud either way, never silent: an entry on
|
|
361
|
+
* a constraint the gate reads as live is refused by name (declared but already
|
|
362
|
+
* switched on), and a muted one this misses is `A47` / `A48` refusing it.
|
|
363
|
+
*/
|
|
364
|
+
function consumerDrivenCandidates(root: JsonObject): Array<{ type: MixedConstraintKind; name: string; reads: string[] }> {
|
|
365
|
+
const TO_MIX: Record<string, string> = {
|
|
366
|
+
rotate: 'mixRotate',
|
|
367
|
+
x: 'mixX',
|
|
368
|
+
y: 'mixY',
|
|
369
|
+
scaleX: 'mixScaleX',
|
|
370
|
+
scaleY: 'mixScaleY',
|
|
371
|
+
shearY: 'mixShearY',
|
|
372
|
+
};
|
|
373
|
+
/** Frame order of a transform timeline's six curve channels. */
|
|
374
|
+
const TRANSFORM_ORDER = ['mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY'];
|
|
375
|
+
const num = (value: unknown, fallback: number): number => (typeof value === 'number' ? value : fallback);
|
|
376
|
+
const transformMixes = (m: JsonObject): Record<string, number> => {
|
|
377
|
+
const mixX = num(m.mixX, 1);
|
|
378
|
+
const mixScaleX = num(m.mixScaleX, 1);
|
|
379
|
+
return {
|
|
380
|
+
mixRotate: num(m.mixRotate, 1),
|
|
381
|
+
mixX,
|
|
382
|
+
mixY: num(m.mixY, mixX),
|
|
383
|
+
mixScaleX,
|
|
384
|
+
mixScaleY: num(m.mixScaleY, mixScaleX),
|
|
385
|
+
mixShearY: num(m.mixShearY, 1),
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
/** The Bezier handle values of channel `c` on a key's `curve`; none when it is linear or stepped. */
|
|
389
|
+
const handles = (key: JsonObject, c: number): number[] =>
|
|
390
|
+
Array.isArray(key.curve) ? [key.curve[4 * c + 1], key.curve[4 * c + 3]].filter((v): v is number => typeof v === 'number') : [];
|
|
391
|
+
const keysOf = (kind: MixedConstraintKind, name: string): JsonObject[] =>
|
|
392
|
+
Object.values(obj(root.animations)).flatMap((animation) => arr(obj(obj(animation)[kind])[name]).map(obj));
|
|
393
|
+
const out: Array<{ type: MixedConstraintKind; name: string; reads: string[] }> = [];
|
|
394
|
+
for (const raw of arr(root.constraints)) {
|
|
395
|
+
const constraint = obj(raw);
|
|
396
|
+
if (typeof constraint.name !== 'string') continue;
|
|
397
|
+
if (constraint.type === 'ik') {
|
|
398
|
+
if (num(constraint.mix, 1) !== 0) continue;
|
|
399
|
+
const lifted = keysOf('ik', constraint.name).some((key) => num(key.mix, 1) !== 0 || handles(key, 0).some((v) => v !== 0));
|
|
400
|
+
if (!lifted) out.push({ type: 'ik', name: constraint.name, reads: ['mix'] });
|
|
401
|
+
} else if (constraint.type === 'transform') {
|
|
402
|
+
const reads = [
|
|
403
|
+
...new Set(
|
|
404
|
+
Object.values(obj(constraint.properties)).flatMap((from) => Object.keys(obj(obj(from).to)).map((to) => TO_MIX[to])),
|
|
405
|
+
),
|
|
406
|
+
]
|
|
407
|
+
.filter((mix): mix is string => mix !== undefined)
|
|
408
|
+
.sort((a, b) => TRANSFORM_ORDER.indexOf(a) - TRANSFORM_ORDER.indexOf(b));
|
|
409
|
+
if (reads.length === 0) continue;
|
|
410
|
+
const setup = transformMixes(constraint);
|
|
411
|
+
if (reads.some((mix) => setup[mix] !== 0)) continue;
|
|
412
|
+
const lifted = keysOf('transform', constraint.name).some((key) => {
|
|
413
|
+
const values = transformMixes(key);
|
|
414
|
+
return reads.some((mix) => values[mix] !== 0 || handles(key, TRANSFORM_ORDER.indexOf(mix)).some((v) => v !== 0));
|
|
415
|
+
});
|
|
416
|
+
if (!lifted) out.push({ type: 'transform', name: constraint.name, reads });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
|
|
334
422
|
/** What omitting one inert physics constraint took with it, for its finding. */
|
|
335
423
|
interface PhysicsOmission {
|
|
336
424
|
/** `animation "<a>" <timeline>`, in file order. */
|
|
@@ -811,11 +899,48 @@ export function ingest(skeleton: unknown, opts: IngestOptions): IngestResult {
|
|
|
811
899
|
);
|
|
812
900
|
}
|
|
813
901
|
|
|
902
|
+
// -- constraints the consumer drives (issue #784) --------------------------
|
|
903
|
+
// A constraint resting muted that no animation switches on is exactly what
|
|
904
|
+
// `A47` / `A48` refuse, and the export cannot say whether it is a leftover or
|
|
905
|
+
// a dial a game turns from code: the two are the same bytes. The rebuild
|
|
906
|
+
// reads it as the consumer's — the reading under which the file is correct —
|
|
907
|
+
// and says so twice: in the rig spec, as the declaration the gate reads, and
|
|
908
|
+
// as a finding naming the constraint, so the author sees what the rebuild is
|
|
909
|
+
// claiming. It is a `judgement` for `DURATION`'s reason: a statement the
|
|
910
|
+
// skeleton does not carry, made and printed rather than hidden, with nothing
|
|
911
|
+
// lost — the rebuilt skeleton is the same bytes either way.
|
|
912
|
+
const carried = new Set(constraints.map((c) => constraintAt(String(c.type), String(c.name))));
|
|
913
|
+
const consumerDrivenMix: JsonObject[] = [];
|
|
914
|
+
const animationCount = Object.keys(obj(root.animations)).length;
|
|
915
|
+
for (const { type, name, reads } of consumerDrivenCandidates(root)) {
|
|
916
|
+
if (!carried.has(constraintAt(type, name))) continue;
|
|
917
|
+
const assertion = type === 'ik' ? 'A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT' : 'A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT';
|
|
918
|
+
consumerDrivenMix.push({
|
|
919
|
+
constraint: name,
|
|
920
|
+
type,
|
|
921
|
+
why:
|
|
922
|
+
'the source skeleton rests it muted and no animation keys its mix above 0; `rigc ingest` reads it as a mix ' +
|
|
923
|
+
'the consumer sets, which an export cannot state. Delete this entry if it is a leftover',
|
|
924
|
+
});
|
|
925
|
+
note(
|
|
926
|
+
'judgement',
|
|
927
|
+
'CONSUMER_DRIVEN_MIX',
|
|
928
|
+
`constraint "${name}" (${type})`,
|
|
929
|
+
`rests at ${reads.map((mix) => `${mix} 0`).join(', ')} and none of the ${animationCount} ` +
|
|
930
|
+
`animation${animationCount === 1 ? '' : 's'} keys its mix above 0, so nothing in this file ever switches it ` +
|
|
931
|
+
'on — and the file cannot say whether that is a leftover or a mix a game sets from code. `build` refuses the ' +
|
|
932
|
+
`shape by name (${assertion}) unless the rig spec says which, so the rig spec now says the consumer drives ` +
|
|
933
|
+
'it, in invariants.consumerDrivenMix, and the gate SKIPs it by name rather than measuring it. If it is a ' +
|
|
934
|
+
`leftover, delete the entry and rest ${type === 'ik' ? 'its mix' : 'a mix it reads'} above 0 or remove the ` +
|
|
935
|
+
'constraint, and the gate measures it again',
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
814
939
|
// -- assemble -------------------------------------------------------------
|
|
815
940
|
const rig: JsonObject = {
|
|
816
941
|
spec: RIG_SPEC_VERSION,
|
|
817
942
|
name: opts.name,
|
|
818
|
-
note: provenanceNote(opts, 'rig'),
|
|
943
|
+
note: provenanceNote(opts, 'rig', consumerDrivenMix.length > 0),
|
|
819
944
|
};
|
|
820
945
|
if (Object.keys(rigHeader).length) rig.skeleton = rigHeader;
|
|
821
946
|
// Between `skeleton` and `bones`, which is where `RIG_KEYS.RigSpec` puts it —
|
|
@@ -826,10 +951,14 @@ export function ingest(skeleton: unknown, opts: IngestOptions): IngestResult {
|
|
|
826
951
|
if (Object.keys(skins).length) rig.skins = skins;
|
|
827
952
|
if (constraints.length) rig.constraints = constraints;
|
|
828
953
|
if (isObj(root.events)) rig.events = { ...root.events };
|
|
829
|
-
// `invariants` is
|
|
830
|
-
// INGEST §2.1 already says what to do about that: leave the block out,
|
|
831
|
-
// an assertion with nothing to measure reports SKIP and never a pass.
|
|
832
|
-
// an invariant here would be certifying a rig
|
|
954
|
+
// `invariants` is absent but for one field. A skeleton states no invariant,
|
|
955
|
+
// and INGEST §2.1 already says what to do about that: leave the block out,
|
|
956
|
+
// because an assertion with nothing to measure reports SKIP and never a pass.
|
|
957
|
+
// Writing an invariant that turns a check ON here would be certifying a rig
|
|
958
|
+
// nobody measured. `consumerDrivenMix` is the other direction — it turns two
|
|
959
|
+
// checks OFF for the constraints named above, which certifies nothing: what it
|
|
960
|
+
// buys is a SKIP by name, and a finding says each one out loud (issue #784).
|
|
961
|
+
if (consumerDrivenMix.length) rig.invariants = { consumerDrivenMix };
|
|
833
962
|
|
|
834
963
|
const motion: JsonObject = {
|
|
835
964
|
spec: MOTION_SPEC_VERSION,
|
|
@@ -1529,20 +1658,25 @@ function ingestAnimation(
|
|
|
1529
1658
|
/**
|
|
1530
1659
|
* A value track of any of the four families. Inverts `compileValueTrack`.
|
|
1531
1660
|
*
|
|
1532
|
-
* ⚠️
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1661
|
+
* ⚠️ An EDITOR omits a channel that equals the parser's default, and the
|
|
1662
|
+
* spec's `v` is positional — so an omission is filled at that channel's
|
|
1663
|
+
* default, which is the value the runtime reads there. The emitter leaves
|
|
1664
|
+
* the same channel out again wherever `PARSER_DEFAULTS` has a row for the
|
|
1665
|
+
* key's kind (issue #716), so the rebuild is the source's own text there and
|
|
1666
|
+
* nothing is said. What is reported, once per track, is a filled channel the
|
|
1667
|
+
* emitter WILL write — a kind with no measured row — because that one is a
|
|
1668
|
+
* restatement rather than a copy and a reader should know which.
|
|
1538
1669
|
*/
|
|
1539
1670
|
const valueTrack = (target: JsonObject, property: string, keys: readonly unknown[], shape: TrackShape, where: string): void => {
|
|
1540
1671
|
const fields = shape.map(([field]) => field);
|
|
1541
1672
|
const out: JsonObject[] = [];
|
|
1542
1673
|
let restated = 0;
|
|
1674
|
+
const family = Object.keys(target)[0];
|
|
1675
|
+
const row = PARSER_DEFAULTS[`${family} ${property} key`];
|
|
1543
1676
|
for (const raw of keys) {
|
|
1544
1677
|
const key = obj(raw);
|
|
1545
1678
|
const entry: JsonObject = { t: timeOf(key) };
|
|
1679
|
+
const filled: string[] = [];
|
|
1546
1680
|
// The zero-field branch: `reset` IS the event, so the key carries no value
|
|
1547
1681
|
// and the spec spells that `null`.
|
|
1548
1682
|
entry.v =
|
|
@@ -1550,13 +1684,21 @@ function ingestAnimation(
|
|
|
1550
1684
|
? null
|
|
1551
1685
|
: shape.map(([field, dflt]) => {
|
|
1552
1686
|
if (key[field] !== undefined) return key[field];
|
|
1553
|
-
|
|
1687
|
+
filled.push(field);
|
|
1554
1688
|
// A string default names another field of THIS key (`mixY` ->
|
|
1555
1689
|
// `mixX`); when that one is absent too the chain ends at 1, which
|
|
1556
1690
|
// is what `ConstraintChannel.dflt` holds for both.
|
|
1557
1691
|
if (typeof dflt !== 'string') return dflt;
|
|
1558
1692
|
return key[dflt] === undefined ? 1 : key[dflt];
|
|
1559
1693
|
});
|
|
1694
|
+
if (filled.length > 0) {
|
|
1695
|
+
// The key as the emitter will hold it — every channel stated — asked
|
|
1696
|
+
// the one question the emitter asks of it.
|
|
1697
|
+
const emitted: JsonObject = { ...key };
|
|
1698
|
+
shape.forEach(([field], i) => (emitted[field] = (entry.v as JsonObject[string][])[i]));
|
|
1699
|
+
const site = { object: emitted, previous: () => null };
|
|
1700
|
+
restated += filled.filter((field) => row === undefined || !parserOmits(row, site, field)).length;
|
|
1701
|
+
}
|
|
1560
1702
|
easing(key, entry);
|
|
1561
1703
|
for (const field of Object.keys(key)) {
|
|
1562
1704
|
if (field === 'time' || field === 'curve' || fields.includes(field)) continue;
|
|
@@ -1571,7 +1713,8 @@ function ingestAnimation(
|
|
|
1571
1713
|
where,
|
|
1572
1714
|
`${restated} channel value(s) the source omits are written out at the parser's default (${shape
|
|
1573
1715
|
.map(([field, dflt]) => `${field}=${String(dflt)}`)
|
|
1574
|
-
.join(', ')}), because the motion spec's \`v\` is positional
|
|
1716
|
+
.join(', ')}), because the motion spec's \`v\` is positional and the emitter has no measured row ` +
|
|
1717
|
+
`for "${family} ${property}" keys to leave them out by — the same values the runtime reads`,
|
|
1575
1718
|
);
|
|
1576
1719
|
}
|
|
1577
1720
|
tracks.push({ ...target, property, keys: out });
|
|
@@ -1966,7 +2109,10 @@ function ingestAnimation(
|
|
|
1966
2109
|
* not all name the same fields — *"state it on every key or on none"*. An
|
|
1967
2110
|
* export does not obey that: it omits a field wherever it equals the default.
|
|
1968
2111
|
* So a field ANY key states is written on EVERY key, at the value the parser
|
|
1969
|
-
* would have read there. Identical semantics,
|
|
2112
|
+
* would have read there. Identical semantics — and, since issue #716, the
|
|
2113
|
+
* same file wherever `PARSER_DEFAULTS` has the key's row, because the
|
|
2114
|
+
* emitter leaves each such value out again. What is still reported is a
|
|
2115
|
+
* value the emitter writes back.
|
|
1970
2116
|
* 2. 🚨 **`rigFlags`.** `compileConstraintTrack` stamps the rig constraint's
|
|
1971
2117
|
* non-default `bendPositive`/`compress`/`stretch` onto a key that omits one
|
|
1972
2118
|
* (issue #273). On rigc's own output that is self-consistent — rigc already
|
|
@@ -2004,19 +2150,16 @@ function constraintGroup(
|
|
|
2004
2150
|
}
|
|
2005
2151
|
}
|
|
2006
2152
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
`${fields.length - stated.size} field(s) the source omits are restated at the parser's default on every key, ` +
|
|
2013
|
-
'because the motion spec requires one field set per track — the same values the runtime reads, spelled out',
|
|
2014
|
-
);
|
|
2015
|
-
}
|
|
2153
|
+
// A field filled on a key the source left it off is said only where the
|
|
2154
|
+
// emitter will write it: where `PARSER_DEFAULTS`' row for the key would
|
|
2155
|
+
// leave it out again, the rebuild is the source's own text (issue #716).
|
|
2156
|
+
const row = PARSER_DEFAULTS[`${group} key`];
|
|
2157
|
+
let restated = 0;
|
|
2016
2158
|
const ks = keys.map((raw) => {
|
|
2017
2159
|
const key = obj(raw);
|
|
2018
2160
|
const entry: JsonObject = { t: typeof key.time === 'number' ? key.time : 0 };
|
|
2019
2161
|
seeT(entry.t as number);
|
|
2162
|
+
const filled: string[] = [];
|
|
2020
2163
|
for (const field of fields) {
|
|
2021
2164
|
if (!stated.has(field)) continue;
|
|
2022
2165
|
if (key[field] !== undefined) entry[field] = key[field];
|
|
@@ -2025,8 +2168,15 @@ function constraintGroup(
|
|
|
2025
2168
|
// `mixY`'s default is the same key's own `mixX`, spelled as that field
|
|
2026
2169
|
// name in the table above.
|
|
2027
2170
|
entry[field] = typeof dflt === 'string' ? (key[dflt] !== undefined ? key[dflt] : defaults[dflt]) : dflt;
|
|
2171
|
+
filled.push(field);
|
|
2028
2172
|
}
|
|
2029
2173
|
}
|
|
2174
|
+
if (filled.length > 0) {
|
|
2175
|
+
const emitted: JsonObject = { ...key };
|
|
2176
|
+
for (const field of fields) if (entry[field] !== undefined) emitted[field] = entry[field];
|
|
2177
|
+
const site = { object: emitted, previous: () => null };
|
|
2178
|
+
restated += filled.filter((field) => row === undefined || !parserOmits(row, site, field)).length;
|
|
2179
|
+
}
|
|
2030
2180
|
if (key.curve === 'stepped') entry.ease = 'stepped';
|
|
2031
2181
|
else if (key.curve !== undefined) entry.curve = key.curve;
|
|
2032
2182
|
for (const field of Object.keys(key)) {
|
|
@@ -2035,6 +2185,16 @@ function constraintGroup(
|
|
|
2035
2185
|
}
|
|
2036
2186
|
return entry;
|
|
2037
2187
|
});
|
|
2188
|
+
if (restated > 0) {
|
|
2189
|
+
note(
|
|
2190
|
+
'lossy',
|
|
2191
|
+
'CONSTRAINT_KEY_RESTATED',
|
|
2192
|
+
where,
|
|
2193
|
+
`${restated} value(s) the source omits are restated at the parser's default, because the motion spec ` +
|
|
2194
|
+
'requires one field set per track and the emitter writes them back — the same values the runtime reads, ' +
|
|
2195
|
+
'spelled out',
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2038
2198
|
out.push({ constraint: name, keys: ks });
|
|
2039
2199
|
}
|
|
2040
2200
|
return out;
|
|
@@ -2052,11 +2212,21 @@ function constraintGroup(
|
|
|
2052
2212
|
* independent compiles byte for byte, and a dated note in a spec would break the
|
|
2053
2213
|
* first rebuild from it.
|
|
2054
2214
|
*/
|
|
2055
|
-
function provenanceNote(opts: IngestOptions, which: 'rig' | 'motion'): string {
|
|
2215
|
+
function provenanceNote(opts: IngestOptions, which: 'rig' | 'motion', consumerDriven = false): string {
|
|
2056
2216
|
const head =
|
|
2057
2217
|
`DECOMPILED from ${opts.source} by \`rigc ingest\` ${opts.version}. Every number here was read out of that ` +
|
|
2058
2218
|
'skeleton; nothing was authored, so this file says what the object IS and nothing about why.';
|
|
2059
2219
|
if (which === 'rig') {
|
|
2220
|
+
// Only where the declaration was written, so every other decompiled rig spec
|
|
2221
|
+
// keeps its note byte for byte.
|
|
2222
|
+
if (consumerDriven) {
|
|
2223
|
+
return (
|
|
2224
|
+
`${head} \`invariants\` holds only \`consumerDrivenMix\`, the constraints this run read as mixes the ` +
|
|
2225
|
+
'consumer sets (each is a CONSUMER_DRIVEN_MIX finding); it turns a refusal into a SKIP and certifies ' +
|
|
2226
|
+
'nothing. A skeleton declares no other invariant, and an assertion with nothing to measure must SKIP ' +
|
|
2227
|
+
'rather than pass.'
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2060
2230
|
return (
|
|
2061
2231
|
`${head} \`invariants\` is deliberately absent — a skeleton declares none, and an assertion with nothing to ` +
|
|
2062
2232
|
'measure must SKIP rather than pass.'
|