HardYards/web/world/js/rigging.selftest.js
type-two 5bfd843b77 Lane B S16: gate 2.2 THE RECORD — the fabric bet is on the paperwork
fabricNoteFor() (rigging.js, pure, headless-tested): one verdict sentence
when the bet provably mattered — cloth on a night whose stones pass the
2mm-weave ruling with real hail damage names the leak; membrane past the
verdict's own 80 kg pond threshold names the trade; silent everywhere a
sentence would be a guess (big stones, dry nights, unknown fabric).
scoreRun carries {fabric, fabricNote}; the invoice grows an unconditional
'fabric flown' row and the verdict prints the note under itself (existing
verdict prose byte-untouched — a.test's verdictMode pins unaffected).
Prep-summary half already shipped in the fresh-eyes review (panel line 2).
Mutations red-then-green: leak gate inverted; pond threshold dropped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:25:22 +10:00

419 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* rigging.selftest.js — assert suite for the prep-phase economy. [Lane B]
*
* Same shape as sail.selftest.js: exports RIGGING_TESTS as [name, fn] pairs so
* one set of asserts runs under both Lane A's selftest.html (via
* js/tests/b.test.js) and node.
*/
import { RiggingSession, FABRIC, fabricNoteFor } from './rigging.js';
import { SailRig, TENSION_MIN, TENSION_MAX } from './sail.js';
import { HARDWARE, START_BUDGET, SPARE_COST } from './contracts.js';
const [CARABINER, SHACKLE, RATED] = HARDWARE;
/** Lane A's real yard (THREADS: "yard layout is now FACT"), trimmed to what the economy needs. */
export const ANCHORS = [
{ id: 'h1', type: 'house', pos: { x: -5, y: 2.6, z: -9.9 } },
{ id: 'h2', type: 'house', pos: { x: 0, y: 2.6, z: -9.9 } },
{ id: 'h3', type: 'house', pos: { x: 5, y: 2.6, z: -9.9 } },
{ id: 't1', type: 'tree', pos: { x: -9, y: 3.2, z: 2 } },
{ id: 't2', type: 'tree', pos: { x: 8, y: 3.1, z: -2 } },
{ id: 'p1', type: 'post', pos: { x: -6.4, y: 3.9, z: 7.4 } },
{ id: 'p2', type: 'post', pos: { x: 5.3, y: 3.9, z: 8 } },
].map((a) => ({ ...a, sway: () => a.pos }));
const session = () => new RiggingSession({ anchors: ANCHORS });
const TESTS = [];
const test = (name, fn) => TESTS.push([name, fn]);
const assert = (cond, msg) => { if (!cond) throw new Error(msg); };
test('rigging four corners charges the cheapest hardware each', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) assert(s.rig(id).ok, `rig ${id} failed`);
assert(s.budget === START_BUDGET - 4 * CARABINER.cost, `budget $${s.budget}`);
assert(s.canStart, 'four corners should be startable');
return `$${START_BUDGET} -> $${s.budget} after four carabiners`;
});
test('a sail has four corners, not five', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id);
const r = s.rig('t1');
assert(!r.ok && r.reason === 'a sail has four corners', `fifth corner allowed: ${JSON.stringify(r)}`);
assert(s.budget === START_BUDGET - 4 * CARABINER.cost, 'refused corner should not be charged');
return 'fifth pick refused and not charged';
});
test('hardware cycles up, charging only the difference', () => {
const s = session();
s.rig('h1');
assert(s.cycleHardware('h1').ok, 'cycle to shackle failed');
assert(s.pickOf('h1').hw === SHACKLE, 'expected shackle');
assert(s.budget === START_BUDGET - SHACKLE.cost, `budget $${s.budget} should be $${START_BUDGET - SHACKLE.cost}`);
s.cycleHardware('h1');
assert(s.pickOf('h1').hw === RATED, 'expected rated shackle');
assert(s.budget === START_BUDGET - RATED.cost, `budget $${s.budget}`);
return `carabiner -> shackle -> rated, paid $${RATED.cost} total`;
});
test('cycling past the top tier wraps and refunds', () => {
const s = session();
s.rig('h1');
s.cycleHardware('h1'); s.cycleHardware('h1'); // -> rated
s.cycleHardware('h1'); // -> wraps to carabiner
assert(s.pickOf('h1').hw === CARABINER, 'expected wrap back to carabiner');
assert(s.budget === START_BUDGET - CARABINER.cost, `budget $${s.budget} — wrap should refund the difference`);
return `wrapped and refunded back to $${s.budget}`;
});
test('unrig refunds exactly what the corner cost', () => {
const s = session();
s.rig('h1');
s.cycleHardware('h1'); s.cycleHardware('h1'); // rated, $30
assert(s.unrig('h1').ok, 'unrig failed');
assert(s.budget === START_BUDGET, `budget $${s.budget} should be back to $${START_BUDGET}`);
assert(!s.isRigged('h1'), 'h1 should be free again');
return 'full refund, no leak';
});
test('spares cost real money and refund', () => {
const s = session();
assert(s.setSpares(1).ok, 'buying a spare failed');
assert(s.budget === START_BUDGET - SPARE_COST, `budget $${s.budget}`);
s.setSpares(0);
assert(s.budget === START_BUDGET && s.spares === 0, 'selling the spare back should restore budget');
return `spare costs $${SPARE_COST}, refunds clean`;
});
test('budget is a real wall', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id); // $20, $60 left
s.cycleHardware('h1'); s.cycleHardware('h1'); // -> rated, $25 more, $35 left
s.cycleHardware('h3'); s.cycleHardware('h3'); // -> rated, $25 more, $10 left
s.cycleHardware('p1'); // -> shackle, $10, $0 left
const broke = s.cycleHardware('p2');
assert(!broke.ok && broke.reason === 'not enough budget', `overspend allowed: ${JSON.stringify(broke)}`);
assert(s.budget === 0, `budget $${s.budget}`);
assert(s.pickOf('p2').hw === CARABINER, 'refused upgrade should not have applied');
return 'refused the upgrade that would have gone negative';
});
// DESIGN.md: "good hardware everywhere is unaffordable. You *will* field one
// dodgy corner — the game is choosing which one." If this ever passes, the
// central economic tension of the game is gone and the budget is decoration.
// contracts.js's HARDWARE comment names this as the shape retuning had to keep.
test('you cannot afford good hardware on all four corners', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id);
let upgraded = 0;
for (const id of ['h1', 'h3', 'p1', 'p2']) if (s.setHardware(id, RATED).ok) upgraded++;
assert(upgraded < 4, `all four corners got rated shackles with $${START_BUDGET} — no compromise left to make`);
assert(upgraded >= 2, `only ${upgraded} rated corners affordable — budget may be too tight to be interesting`);
return `$${START_BUDGET} buys ${upgraded}/4 rated corners, then you are choosing your weak link`;
});
// ---- the site switch (SPRINT11 — Lane A's setBudget/setWorld asks) ----------
test('setBudget re-banks the shop and clears the night before it', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id); // $20 of carabiners hanging
assert(s.budget === START_BUDGET - 20, `budget $${s.budget}`);
s.setBudget(150); // a new night, a fatter wallet
assert(s.budget === 150, `re-banked budget is $${s.budget}, expected $150`);
// The bug this exists to stop: a re-banked wallet still holding last night's
// rig would read $150 with four corners already up — hardware nobody paid for.
assert(s.picks.length === 0, `${s.picks.length} picks survived the re-bank — this wallet never paid for them`);
// and the new bank has to STICK through a reset, or "play again" hands back $80
s.rig('h1'); s.reset();
assert(s.budget === 150, `reset fell back to $${s.budget} — setBudget must move the START budget, not just the balance`);
return `re-banked to $150, prep phase clean, sticks through reset`;
});
test('setAnchors moves the session to a new yard and drops the old picks', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id);
// the corner block: a different anchor SET, and no h1/p1 anywhere in it
const CORNER_BLOCK = [
{ id: 'q1', type: 'post', pos: { x: -3.95, y: 3.94, z: -2.82 } },
{ id: 'q3', type: 'post', pos: { x: 2.25, y: 3.99, z: 4.5 } },
{ id: 'cb1', type: 'post', pos: { x: -8.41, y: 2.29, z: -3 } },
{ id: 'cp2', type: 'post', pos: { x: -8.41, y: 1.68, z: -5.61 } },
].map((a) => ({ ...a, sway: () => a.pos }));
s.setAnchors(CORNER_BLOCK);
// `p4` on the backyard is not `p4` on the corner block, and `h1` is nowhere:
// picks are anchor IDs, so carrying them across a site rigs a quad from anchors
// that do not exist.
assert(s.picks.length === 0, `${s.picks.length} backyard picks followed the session to the corner block`);
assert(!s.rig('h1').ok, 'h1 is a backyard anchor and must not rig on the corner block');
assert(s.rig('cb1').ok, 'cb1 is a corner-block anchor and should rig');
return 'session followed the site; stale picks did not';
});
test('setHardware THROWS on hardware the shop does not sell', () => {
// SPRINT13, QA pass: it returned {ok:false} and callers that didn't check
// sailed on with the old tier still hanging. balance.test's own comment
// records the bite — "setHardware fails silently, cheap hardware stays on,
// p2/p4 tear off" — a cascade reported as a finding. A wrong tier is
// invisible in the result, so this one must not be ignorable.
const s = session();
s.rig('h1');
let threw = null;
try { s.setHardware('h1', { name: 'titanium', cost: 0, rating: 99999 }); }
catch (e) { threw = e; }
assert(threw, 'a hardware object that is not in HARDWARE was accepted silently');
assert(threw instanceof TypeError, `expected a TypeError, got ${threw?.constructor?.name}`);
assert(s.pickOf('h1').hw === CARABINER, 'the refused tier must not have been applied');
// A LOOKALIKE is the real trap: same shape, same numbers, wrong identity.
let threw2 = null;
try { s.setHardware('h1', { ...CARABINER }); } catch (e) { threw2 = e; }
assert(threw2, 'a copy of a real HARDWARE entry was accepted — the shop compares by identity');
// and the player-reachable failures stay {ok:false}, not throws
assert(s.setHardware('p3', RATED).ok === false, 'an unrigged corner should fail, not throw');
return 'unknown tiers throw; player-reachable failures still return {ok:false}';
});
test('standing load: the tension dial has a price, and the panel can see it', () => {
// D, SPRINT13: "the panel never shows standing kN at the chosen tension, so
// the first lesson always costs ~$60" — at 1.4 the standing load ALONE rips
// corners off during prep, on the calm day. This is the number that makes
// that visible before the money is spent.
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id);
s.setTension(0.7);
const loose = s.standingLoads();
s.setTension(1.4);
const tight = s.standingLoads();
assert(loose && tight, 'a closed quad must have standing loads');
assert(Object.keys(tight).length === 4, `expected 4 corners, got ${Object.keys(tight).length}`);
for (const id of Object.keys(tight)) {
assert(Number.isFinite(tight[id]) && tight[id] > 0, `${id}: standing load ${tight[id]} is not a real load`);
// The whole point: drum-tight costs more at rest than loose does. If this
// ever ties, the dial is decoration and D's $60 lesson teaches nothing.
assert(tight[id] > loose[id],
`${id}: standing load at tension 1.4 (${tight[id].toFixed(0)} N) is not above tension 0.7 ` +
`(${loose[id].toFixed(0)} N) — the dial has no static price, so the prep readout is a lie`);
}
// still air means still air: no wind, no gust, nothing that could be mistaken
// for a forecast. Deterministic — same picks and dial, same number, always.
s.setTension(1.4);
const again = s.standingLoads();
for (const id of Object.keys(tight)) {
assert(Math.abs(again[id] - tight[id]) < 1e-9, `${id}: standing load is not deterministic`);
}
const avg = Object.values(tight).reduce((a, b) => a + b, 0) / 4;
return `tension 0.7 -> 1.4 lifts the standing load to ≈${(avg / 1000).toFixed(2)} kN/corner, repeatably`;
});
test('standing load is null until the quad is closed', () => {
const s = session();
assert(s.standingLoads() === null, 'no picks should have no standing load');
for (const id of ['h1', 'h3', 'p1']) s.rig(id);
assert(s.standingLoads() === null, 'three corners is not a sail — there is no load to read');
s.rig('p2');
assert(s.standingLoads() !== null, 'four corners is a sail and must report');
return 'null until four corners, then real';
});
test('spares are consumable and finite — you cannot take one you did not buy', () => {
// D's live-storm find, SPRINT13: interact.js handed out unlimited free spares
// because it never read the count. This is the count's half of the fix — a
// spare economy that can actually run dry.
const s = session();
assert(s.sparesRemaining === 0, 'a fresh session has no spares on the table');
assert(s.takeSpare().ok === false, 'took a spare that was never bought — the QA bug, in one line');
s.setSpares(2);
assert(s.sparesRemaining === 2, `bought 2, table shows ${s.sparesRemaining}`);
assert(s.takeSpare().ok, 'first spare should come off the table');
assert(s.takeSpare().ok, 'second spare should come off the table');
assert(s.sparesRemaining === 0, `table should be empty, shows ${s.sparesRemaining}`);
const dry = s.takeSpare();
assert(!dry.ok && /no spares/.test(dry.reason), `a third take must fail, got ${JSON.stringify(dry)}`);
// The refund half: main.js pays back "a spare you never had to use" as
// session.spares × SPARE_COST. A consumed spare must not still be refunded —
// so a taken spare has to leave the count, not just flip a used flag.
const s2 = session();
s2.setSpares(3);
s2.takeSpare();
assert(s2.spares === 2, `after taking 1 of 3, refundable count must be 2, is ${s2.spares}`);
return 'spares run dry, and a used spare is no longer refundable';
});
test('picks come back ring-ordered however you click them', () => {
const s = session();
// deliberately crossing order: two diagonals first
for (const id of ['h1', 'p2', 'h3', 'p1']) s.rig(id);
const ids = s.picks.map((p) => p.anchorId);
// a valid ring puts h1 opposite p2 (they are diagonal across the yard)
const opposite = ids[(ids.indexOf('h1') + 2) % 4];
assert(opposite === 'p2', `h1 should sit opposite p2 in the ring, got ${ids.join(',')}`);
return `clicked h1,p2,h3,p1 -> ring ${ids.join(' -> ')}`;
});
test('tension clamps to the rigging range', () => {
const s = session();
assert(s.setTension(99) === TENSION_MAX, 'over-tight should clamp');
assert(s.setTension(0) === TENSION_MIN, 'over-loose should clamp');
s.setTension(1.15);
assert(s.tension === 1.15, 'in-range tension should pass through');
return `clamped to ${TENSION_MIN}..${TENSION_MAX}`;
});
test('commit hands a working rig to the sim', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id);
s.setHardware('h1', RATED);
s.setTension(1.1);
const rig = s.commit(new SailRig({ anchors: ANCHORS }));
assert(rig.rigged, 'rig should be rigged');
assert(rig.corners.length === 4, 'rig should have four corners');
assert(rig.tension === 1.1, `rig tension ${rig.tension}`);
assert(rig.corners.find((c) => c.anchorId === 'h1').hw === RATED, 'h1 should have carried its rated shackle into the sim');
const wind = { sample: () => ({ x: 0, y: 0, z: 12 }) };
for (let i = 0; i < 240; i++) rig.step(1 / 60, wind, i / 60);
assert(rig.corners.every((c) => Number.isFinite(c.load)), 'committed rig went NaN');
return `committed and stepped 4 s clean over the real yard, max load ${(rig.maxLoad() / 1000).toFixed(2)} kN`;
});
test('commit refuses an unfinished rig', () => {
const s = session();
s.rig('h1'); s.rig('h3');
let threw = false;
try { s.commit(new SailRig({ anchors: ANCHORS })); } catch { threw = true; }
assert(threw, 'committing two corners should throw');
return 'two corners refused';
});
test('summary names the weak link for the HUD', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id);
s.setHardware('h1', RATED); s.setHardware('h3', SHACKLE); s.setHardware('p1', SHACKLE);
const sum = s.summary;
assert(sum.weakest === 'p2', `weakest should be the lone carabiner p2, got ${sum.weakest}`);
assert(sum.corners.length === 4, 'summary should list four corners');
return `weak link flagged: ${sum.weakest}, $${sum.budget} left`;
});
test("weak link is rating × ratingHint, not click order (D's #7)", () => {
// Corner-block shape: uniform hardware, one anchor made of lies. The old
// reduce compared bare hw.rating with strict <, so four identical carabiners
// flagged whichever was clicked FIRST — on site_02 it pointed at cb1 only
// because D happened to click cb1 first. The label must follow the product
// sail.js actually fails on: hw.rating × anchor.ratingHint.
const anchors = [
{ id: 'q1', type: 'post', pos: { x: -3, y: 4, z: -3 }, ratingHint: 1 },
{ id: 'q2', type: 'post', pos: { x: 3, y: 4, z: -3 }, ratingHint: 1 },
{ id: 'q3', type: 'post', pos: { x: 3, y: 4, z: 3 }, ratingHint: 1 },
{ id: 'cb1', type: 'carport', pos: { x: -3, y: 2.3, z: 3 }, ratingHint: 0.22 },
].map((a) => ({ ...a, sway: () => a.pos }));
const s = new RiggingSession({ anchors });
// click the honest steel FIRST — the pre-fix code flags q1 here, forever
for (const id of ['q1', 'q2', 'q3', 'cb1']) assert(s.rig(id).ok, `rig ${id} failed`);
assert(s.summary.weakest === 'cb1',
`four identical carabiners: the weak link is the 0.22 carport beam, got ${s.summary.weakest}`);
// And better hardware on the lying anchor can make it genuinely NOT the weak
// link: rated 6500 × 0.22 = 1430 N still beats carabiner 1200 × 1.0 — the
// label follows the product, not the price tag and not the hint alone.
s.setHardware('cb1', RATED);
assert(s.summary.weakest === 'q1',
`rated@0.22 (1430 N) out-rates carabiner@1.0 (1200 N) — expected q1, got ${s.summary.weakest}`);
return 'weak link tracks rating × hint through hardware changes';
});
test('reset() returns a used session to a fresh prep phase', () => {
const s = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) s.rig(id);
s.setHardware('h1', RATED); s.setTension(1.2); s.setSpares(1);
assert(s.budget < START_BUDGET, 'setup should have spent money');
s.reset();
assert(s.budget === START_BUDGET, `budget not restored: $${s.budget}`);
assert(s.picks.length === 0, 'picks not cleared');
assert(s.tension === 1.0 && s.spares === 0, 'tension/spares not reset');
// and it's actually usable again, not just zeroed
assert(s.rig('t1').ok && s.canStart === false, 'session not rig-able after reset');
return 'budget, picks, tension, spares all fresh; rig-able again';
});
test('fabric is a bet the F key can actually place (SPRINT15 — the dead mechanic)', () => {
const s = session();
assert(s.fabric.id === 'cloth', `default fabric should be cloth, got ${s.fabric.id}`);
// the exact call the F key makes: cycle to the other entry by id
const next = FABRIC[(FABRIC.indexOf(s.fabric) + 1) % FABRIC.length];
assert(s.setFabric(next.id).ok, 'cycling to membrane refused');
assert(s.fabric.id === 'membrane' && s.fabric.porosity === 0, 'membrane not applied');
assert(s.budget === START_BUDGET, 'fabric is a bet, not a purchase — budget must not move');
assert(s.summary.fabric.id === 'membrane', 'the panel reads fabric off summary and must see the change');
// wrap-around brings it home, and garbage is refused loudly
assert(s.setFabric(FABRIC[(FABRIC.indexOf(s.fabric) + 1) % FABRIC.length].id).ok && s.fabric.id === 'cloth', 'cycle should wrap back to cloth');
assert(s.setFabric('hessian').ok === false, 'unknown fabric must refuse, not default');
return 'cloth -> membrane -> cloth, $0 both ways, hessian refused';
});
// --- SPRINT16 gate 2.2: the fabric on the record ---------------------------
// The invoice row is unconditional wiring (main.js/hud.js, verified live);
// what this pins is the VERDICT's fabric sentence — fabricNoteFor speaks only
// when the bet provably mattered, and stays silent everywhere a sentence
// would be a guess. Storm hail sizes are the shipped ones: southerly pea hail
// 0.7 (cloth blocks 74% — hailBlockFor's measured number), wild night 1.3 and
// ice night 1.4 (both fabrics block 100%).
test('fabricNoteFor: speaks only when the bet mattered, silent when a sentence would guess', () => {
const [CLOTH, MEMBRANE] = FABRIC;
// the spec's case: a pea-hail night flown in cloth, hail cost the bed real HP
const leak = fabricNoteFor({ fabric: CLOTH, hailSize: 0.7, dmg: { hail: 22, rain: 3 } });
assert(leak && leak.includes(CLOTH.name), `leak note must name the fabric flown, got: ${leak}`);
assert(leak.includes('22'), `leak note must say what the leak cost, got: ${leak}`);
// big stones cannot pass the weave — cloth blocked 100%, hail damage came
// from where the sail wasn't, and blaming the weave for it would be a lie
assert(fabricNoteFor({ fabric: CLOTH, hailSize: 1.3, dmg: { hail: 40, rain: 5 } }) === null,
'cloth blamed for big-stone hail — hailBlockFor(1.3, 0.3) is 1.0, nothing leaked');
// a leak that cost under the threshold is not worth a verdict sentence
assert(fabricNoteFor({ fabric: CLOTH, hailSize: 0.7, dmg: { hail: fabricNoteFor.LEAK_MATTERS_HP - 1, rain: 0 } }) === null,
'a trivial leak got a sentence — the threshold is not being read');
// membrane's price is the pond, and only a pond the verdict itself would name
const pond = fabricNoteFor({ fabric: MEMBRANE, hailSize: 0.7, dmg: { hail: 0, rain: 2 }, pondPeak: 240 });
assert(pond && pond.includes(MEMBRANE.name) && pond.includes('240'), `pond note must name membrane and the kg held, got: ${pond}`);
assert(fabricNoteFor({ fabric: MEMBRANE, hailSize: 0.7, dmg: { hail: 0, rain: 2 }, pondPeak: 40 }) === null,
'membrane got a sentence over a pond the verdict would not even mention');
// no hail data (a dry night), unknown fabric, missing fabric: silence, not a throw
assert(fabricNoteFor({ fabric: CLOTH, dmg: { hail: 30, rain: 0 } }) === null, 'no hail tonight but the weave got blamed');
assert(fabricNoteFor({ fabric: { id: 'hessian', name: 'hessian', porosity: 0.5 } }) === null, 'unknown fabric must be silent');
assert(fabricNoteFor({}) === null && fabricNoteFor() === null, 'missing fabric must be silent, not a throw');
return `leak note fires at 0.7-size stones + >=${fabricNoteFor.LEAK_MATTERS_HP} HP; pond note at >${fabricNoteFor.POND_MATTERS_KG} kg; all else silent`;
});
export const RIGGING_TESTS = TESTS;
export function runRiggingSelftest() {
const results = TESTS.map(([name, fn]) => {
try { return { name, pass: true, detail: fn() || '' }; }
catch (e) { return { name, pass: false, detail: e.message }; }
});
return { pass: results.every((r) => r.pass), results };
}
function report(out) {
const lines = out.results.map(
(r) => `${r.pass ? 'PASS' : 'FAIL'} ${r.name}${r.detail ? `\n ${r.detail}` : ''}`
);
return `${lines.join('\n')}\n\n${out.pass ? 'ALL GREEN' : 'FAILURES'}${out.results.filter((r) => r.pass).length}/${out.results.length}`;
}
if (typeof process !== 'undefined' && process.versions?.node && import.meta.filename === process.argv[1]) {
const out = runRiggingSelftest();
console.log(report(out));
process.exit(out.pass ? 0 : 1);
}
export { report };