@mark1russell7/rigor-bench 0.2.13 → 0.2.15
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 +83 -0
- package/build/index.d.ts +548 -236
- package/build/index.d.ts.map +1 -1
- package/build/index.js +669 -2
- package/build/index.js.map +1 -1
- package/package.json +2 -2
package/build/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as checkPairedTime, r as firstArm, t as caseRatios } from "./paired.js";
|
|
2
|
-
import { bootstrapMedian, bootstrapTwo, compareRatios, median, medianInterval, quantile, quantileInterval, quantileSamples, wilson } from "@mark1russell7/rigor-statistics";
|
|
2
|
+
import { bootstrapMedian, bootstrapTwo, compareRatios, holm, median, medianInterval, partialConjunction, quantile, quantileInterval, quantileSamples, signTest, signTestPower, wilson } from "@mark1russell7/rigor-statistics";
|
|
3
3
|
import { random } from "@mark1russell7/rigor-generators";
|
|
4
4
|
// src/budget.ts
|
|
5
5
|
/**
|
|
@@ -212,6 +212,216 @@ function budgetLooksVerdict(rounds, aa, options) {
|
|
|
212
212
|
function round$2(x) {
|
|
213
213
|
return Math.round(x * 1e4) / 1e4;
|
|
214
214
|
}
|
|
215
|
+
// src/gate.ts
|
|
216
|
+
/**
|
|
217
|
+
* The gate of a bench: the result that a check of a merge reads. A verdict gives one of the words of `LimitOutcome` for each
|
|
218
|
+
* case, and the gate gives "pass", "block" or "no read". A case with no outcome does not pass.
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* This function gives the gate of a list of case outcomes.
|
|
222
|
+
* - The result is "block" when a case is `beyond`.
|
|
223
|
+
* - The result is "pass" when each case is `within`.
|
|
224
|
+
* - Each other result is "no read", and so is a list with no cases. A caller does not count "no read" as a pass.
|
|
225
|
+
*/
|
|
226
|
+
function gateOf(cases) {
|
|
227
|
+
const names = (outcome) => cases.filter((c) => c.outcome === outcome).map((c) => c.case);
|
|
228
|
+
const blocks = names("beyond");
|
|
229
|
+
const noRead = names("cannot decide");
|
|
230
|
+
if (cases.length === 0) return {
|
|
231
|
+
outcome: "no read",
|
|
232
|
+
reason: "The gate has no cases.",
|
|
233
|
+
blocks,
|
|
234
|
+
noRead
|
|
235
|
+
};
|
|
236
|
+
if (blocks.length > 0) return {
|
|
237
|
+
outcome: "block",
|
|
238
|
+
reason: `${blocks.length} of ${cases.length} cases are beyond the claim. The first is ${JSON.stringify(blocks[0])}.`,
|
|
239
|
+
blocks,
|
|
240
|
+
noRead
|
|
241
|
+
};
|
|
242
|
+
if (noRead.length > 0) return {
|
|
243
|
+
outcome: "no read",
|
|
244
|
+
reason: `${noRead.length} of ${cases.length} cases have no outcome. The first is ${JSON.stringify(noRead[0])}.`,
|
|
245
|
+
blocks,
|
|
246
|
+
noRead
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
outcome: "pass",
|
|
250
|
+
reason: `Each of the ${cases.length} cases is within the claim.`,
|
|
251
|
+
blocks,
|
|
252
|
+
noRead
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const READ_OF = {
|
|
256
|
+
within: "not slower",
|
|
257
|
+
beyond: "slower",
|
|
258
|
+
"cannot decide": "no read"
|
|
259
|
+
};
|
|
260
|
+
/** The looks of `notSlowerVerdict`: the rounds in each job. The level of the family has an equal share at each look. */
|
|
261
|
+
const GATE_LOOKS = [
|
|
262
|
+
12,
|
|
263
|
+
24,
|
|
264
|
+
48
|
|
265
|
+
];
|
|
266
|
+
/** This function examines the options. */
|
|
267
|
+
function checkOptions(level, looks, need) {
|
|
268
|
+
if (!(level > 0 && level < 1)) throw new RangeError(`notSlowerVerdict: the level is above 0 and below 1, not ${level}.`);
|
|
269
|
+
if (looks.length === 0) throw new RangeError("notSlowerVerdict: the list of looks is empty.");
|
|
270
|
+
let last = 0;
|
|
271
|
+
for (const rounds of looks) {
|
|
272
|
+
if (!(Number.isInteger(rounds) && rounds > last && rounds % 2 === 0)) throw new RangeError(`notSlowerVerdict: the looks are even whole numbers in rising order, not ${rounds} after ${last}.`);
|
|
273
|
+
last = rounds;
|
|
274
|
+
}
|
|
275
|
+
if (!(Number.isInteger(need) && need >= 1)) throw new RangeError(`notSlowerVerdict: the count of jobs is a whole number of 1 or more, not ${need}.`);
|
|
276
|
+
}
|
|
277
|
+
/** This function gives the records of one case of one job in the order of the rounds. It refuses a round that comes twice. */
|
|
278
|
+
function casesOf(records) {
|
|
279
|
+
const out = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const record of records) {
|
|
281
|
+
checkPairedTime(record);
|
|
282
|
+
const list = out.get(record.case) ?? [];
|
|
283
|
+
if (list.some((r) => r.round === record.round)) throw new RangeError(`notSlowerVerdict: the round ${record.round} of ${JSON.stringify(record.case)} comes twice.`);
|
|
284
|
+
list.push(record);
|
|
285
|
+
out.set(record.case, list);
|
|
286
|
+
}
|
|
287
|
+
for (const list of out.values()) list.sort((a, b) => a.round - b.round);
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* This function gives the differences of the first `rounds` rounds: log(b / a) of the candidate minus log(b / a) of the A/A arm.
|
|
292
|
+
* A pair has two records with the same first arm, because a difference of two orders holds the effect of the order.
|
|
293
|
+
*/
|
|
294
|
+
function differences(candidate, aa, rounds) {
|
|
295
|
+
const out = [];
|
|
296
|
+
const ofOrder = (list, first) => list.slice(0, rounds).filter((r) => r.first === first);
|
|
297
|
+
for (const first of ["a", "b"]) {
|
|
298
|
+
const left = ofOrder(candidate, first);
|
|
299
|
+
const right = ofOrder(aa, first);
|
|
300
|
+
for (let i = 0; i < Math.min(left.length, right.length); i++) {
|
|
301
|
+
const c = left[i];
|
|
302
|
+
const a = right[i];
|
|
303
|
+
out.push(Math.log(c.b / c.a) - Math.log(a.b / a.a));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
/** This function gives the read of one job. A job that has not reached the look gives a p of 1 in both directions. */
|
|
309
|
+
function jobRead(job, diffs, margin, rounds) {
|
|
310
|
+
if (diffs.length < rounds) return {
|
|
311
|
+
job,
|
|
312
|
+
rounds: diffs.length,
|
|
313
|
+
below: 0,
|
|
314
|
+
above: 0,
|
|
315
|
+
notSlower: 1,
|
|
316
|
+
slower: 1,
|
|
317
|
+
centre: void 0
|
|
318
|
+
};
|
|
319
|
+
const below = diffs.filter((d) => d < margin).length;
|
|
320
|
+
const above = diffs.filter((d) => d > margin).length;
|
|
321
|
+
return {
|
|
322
|
+
job,
|
|
323
|
+
rounds: diffs.length,
|
|
324
|
+
below,
|
|
325
|
+
above,
|
|
326
|
+
notSlower: signTest(below, diffs.length - below),
|
|
327
|
+
slower: signTest(above, diffs.length - above),
|
|
328
|
+
centre: median(diffs)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** This function gives the words of the facts of one job, for a reason. */
|
|
332
|
+
function jobFacts(read) {
|
|
333
|
+
if (read.centre === void 0) return `job ${read.job} has ${read.rounds} pairs`;
|
|
334
|
+
return `job ${read.job} has ${read.below} of ${read.rounds} pairs below the margin, centre ${Math.round(read.centre * 1e5) / 1e3} %`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* This function gives the gate of the claim "the candidate is not slower than the A/A arm by more than the margin" for each
|
|
338
|
+
* case. A one-sided bound answers the claim. Thus a case with its whole interval under the margin reads "not slower", also
|
|
339
|
+
* when its interval holds 1.
|
|
340
|
+
*
|
|
341
|
+
* - In each job, the sign test compares the median of the differences with the margin. It uses no random numbers.
|
|
342
|
+
* - A case reads in `jobs` jobs or more: `partialConjunction` joins the p of each job, with no condition on how they depend.
|
|
343
|
+
* - The level is for the family. Each look has `level / looks` of it, and `holm` corrects each look over all cases.
|
|
344
|
+
* - The first look with an outcome decides a case. Later rounds do not change it. Each look uses the first rounds of a job.
|
|
345
|
+
* - The result is "block" when a case reads "slower", and "pass" when each case reads "not slower".
|
|
346
|
+
* - Each other result is "no read". The field `more` gives the next look of each case. After the last look, the caller adds jobs.
|
|
347
|
+
*
|
|
348
|
+
* The level needs independent rounds. Rounds with a shared cause, for example a slow computer for one job, do not have it.
|
|
349
|
+
* A job that a caller adds after the last look is one more look, and the level does not count it.
|
|
350
|
+
*/
|
|
351
|
+
function notSlowerVerdict(jobs, options) {
|
|
352
|
+
const level = options.level ?? .05;
|
|
353
|
+
const looks = options.looks ?? GATE_LOOKS;
|
|
354
|
+
const need = options.jobs ?? 2;
|
|
355
|
+
checkOptions(level, looks, need);
|
|
356
|
+
const marginOf = (name) => {
|
|
357
|
+
const margin = typeof options.margin === "number" ? options.margin : options.margin(name);
|
|
358
|
+
if (!(margin >= 0 && Number.isFinite(margin))) throw new RangeError(`notSlowerVerdict: the margin of ${JSON.stringify(name)} is finite and 0 or more, not ${margin}.`);
|
|
359
|
+
return margin;
|
|
360
|
+
};
|
|
361
|
+
const records = jobs.map((job) => ({
|
|
362
|
+
job: job.job,
|
|
363
|
+
candidate: casesOf(job.candidate),
|
|
364
|
+
aa: casesOf(job.aa)
|
|
365
|
+
}));
|
|
366
|
+
const names = [...new Set(records.flatMap((job) => [...job.candidate.keys()]))].sort();
|
|
367
|
+
const margins = names.map(marginOf);
|
|
368
|
+
const decided = /* @__PURE__ */ new Map();
|
|
369
|
+
const reached = /* @__PURE__ */ new Map();
|
|
370
|
+
const each = level / looks.length;
|
|
371
|
+
for (const rounds of looks) {
|
|
372
|
+
const reads = names.map((name, i) => records.map((job) => jobRead(job.job, differences(job.candidate.get(name) ?? [], job.aa.get(name) ?? [], rounds), margins[i], rounds)));
|
|
373
|
+
const joined = (pick) => reads.map((list) => list.length < need ? 1 : partialConjunction(list.map(pick), need));
|
|
374
|
+
const within = holm(joined((read) => read.notSlower), each);
|
|
375
|
+
const beyond = holm(joined((read) => read.slower), each);
|
|
376
|
+
names.forEach((name, i) => {
|
|
377
|
+
const list = reads[i];
|
|
378
|
+
if (!reached.has(name) || list.some((read) => read.rounds >= rounds)) reached.set(name, list);
|
|
379
|
+
if (decided.has(name)) return;
|
|
380
|
+
if (within[i]?.reject === true) decided.set(name, {
|
|
381
|
+
outcome: "within",
|
|
382
|
+
look: rounds,
|
|
383
|
+
reads: list
|
|
384
|
+
});
|
|
385
|
+
else if (beyond[i]?.reject === true) decided.set(name, {
|
|
386
|
+
outcome: "beyond",
|
|
387
|
+
look: rounds,
|
|
388
|
+
reads: list
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const cases = names.map((name, i) => {
|
|
393
|
+
const decision = decided.get(name);
|
|
394
|
+
const outcome = decision?.outcome ?? "cannot decide";
|
|
395
|
+
const reads = decision?.reads ?? reached.get(name) ?? [];
|
|
396
|
+
const facts = reads.map(jobFacts).join(", ");
|
|
397
|
+
const where = decision === void 0 ? `no look of ${looks.join(", ")} rounds decides it` : `the look of ${decision.look} rounds decides it`;
|
|
398
|
+
return {
|
|
399
|
+
case: name,
|
|
400
|
+
outcome,
|
|
401
|
+
read: READ_OF[outcome],
|
|
402
|
+
margin: margins[i],
|
|
403
|
+
look: decision?.look,
|
|
404
|
+
jobs: reads,
|
|
405
|
+
reason: `The case ${JSON.stringify(name)} reads "${READ_OF[outcome]}": ${where} (${facts}).`
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
const more = cases.flatMap((c) => {
|
|
409
|
+
if (c.outcome !== "cannot decide") return [];
|
|
410
|
+
const next = looks.find((rounds) => records.some((job) => differences(job.candidate.get(c.case) ?? [], job.aa.get(c.case) ?? [], rounds).length < rounds));
|
|
411
|
+
return next === void 0 ? [] : [{
|
|
412
|
+
case: c.case,
|
|
413
|
+
rounds: next
|
|
414
|
+
}];
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
...gateOf(cases),
|
|
418
|
+
level,
|
|
419
|
+
looks,
|
|
420
|
+
need,
|
|
421
|
+
cases,
|
|
422
|
+
more
|
|
423
|
+
};
|
|
424
|
+
}
|
|
215
425
|
// src/control.ts
|
|
216
426
|
/**
|
|
217
427
|
* The load factor of a run, read from a control. Runs on shared machines differ by load. A change does not touch a control.
|
|
@@ -1089,6 +1299,463 @@ function armVerdicts(run, aaPair, options = {}) {
|
|
|
1089
1299
|
for (const [pair, records] of Object.entries(run.pairs)) if (pair !== aaPair) out[pair] = timeVerdict(balanceOrders(records).records, aa, options);
|
|
1090
1300
|
return out;
|
|
1091
1301
|
}
|
|
1302
|
+
// src/measures.ts
|
|
1303
|
+
/**
|
|
1304
|
+
* The check that a trick does not buy a measure. A control is a strategy made to win one measure and nothing else.
|
|
1305
|
+
* A real entry that wins that measure can do the same trick, so the win alone tells little. This check reads the controls of a
|
|
1306
|
+
* bench, and it tells which wins need a partner measure beside them. It also tells which measures separate nothing.
|
|
1307
|
+
*
|
|
1308
|
+
* A test compares paired values with the sign test of `rigor-statistics` and with the Holm correction. The keys of one stratum come
|
|
1309
|
+
* from one family of inputs, so they are not independent draws. The p of a test holds only under the sign test's own null: each
|
|
1310
|
+
* untied key is a fair coin. A result is about these keys, and this check claims nothing about other inputs.
|
|
1311
|
+
*/
|
|
1312
|
+
const DEFAULT_ALPHA = .05;
|
|
1313
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1314
|
+
function checkedDate(name, date) {
|
|
1315
|
+
const time = Date.parse(`${date}T00:00:00Z`);
|
|
1316
|
+
if (!DATE.test(date) || !Number.isFinite(time) || new Date(time).toISOString().slice(0, 10) !== date) throw new RangeError(`checkMeasures: ${name} is a date as YYYY-MM-DD, not ${JSON.stringify(date)}.`);
|
|
1317
|
+
return date;
|
|
1318
|
+
}
|
|
1319
|
+
/** This function refuses the rules that contradict each other, because a wrong rule reads as a passed check. */
|
|
1320
|
+
function checkedRules(options) {
|
|
1321
|
+
const controls = new Set(options.controls);
|
|
1322
|
+
const rules = /* @__PURE__ */ new Map();
|
|
1323
|
+
for (const rule of options.rules) {
|
|
1324
|
+
if (rules.has(rule.measure)) throw new RangeError(`checkMeasures: the measure ${JSON.stringify(rule.measure)} has two rules.`);
|
|
1325
|
+
rules.set(rule.measure, rule);
|
|
1326
|
+
}
|
|
1327
|
+
for (const rule of options.rules) {
|
|
1328
|
+
const name = JSON.stringify(rule.measure);
|
|
1329
|
+
for (const control of rule.controls) if (!controls.has(control)) throw new RangeError(`checkMeasures: the control ${JSON.stringify(control)} of ${name} is not in the controls of the options.`);
|
|
1330
|
+
for (const partner of rule.partners) {
|
|
1331
|
+
if (partner === rule.measure) throw new RangeError(`checkMeasures: ${name} names itself as a partner.`);
|
|
1332
|
+
if (!rules.has(partner)) throw new RangeError(`checkMeasures: the partner ${JSON.stringify(partner)} of ${name} has no rule.`);
|
|
1333
|
+
}
|
|
1334
|
+
if (rule.resolution !== void 0 && !(Number.isFinite(rule.resolution) && rule.resolution >= 0)) throw new RangeError(`checkMeasures: the resolution of ${name} is finite and 0 or more, not ${rule.resolution}.`);
|
|
1335
|
+
if (rule.noControl !== void 0) {
|
|
1336
|
+
if (rule.controls.length > 0) throw new RangeError(`checkMeasures: ${name} has controls and also noControl.`);
|
|
1337
|
+
checkedDate(`the date of ${name}`, rule.noControl.until);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return rules;
|
|
1341
|
+
}
|
|
1342
|
+
/** This function groups the rows. It refuses a value that is not finite, a repeat, and a key in two strata. */
|
|
1343
|
+
function groupedRows(rows) {
|
|
1344
|
+
const strata = /* @__PURE__ */ new Map();
|
|
1345
|
+
const measures = /* @__PURE__ */ new Map();
|
|
1346
|
+
for (const row of rows) {
|
|
1347
|
+
const where = `${JSON.stringify(row.key)} of ${JSON.stringify(row.group)} on ${JSON.stringify(row.measure)}`;
|
|
1348
|
+
if (!Number.isFinite(row.value)) throw new RangeError(`checkMeasures: the value of ${where} is not finite: ${row.value}.`);
|
|
1349
|
+
if (strata.has(row.key) && strata.get(row.key) !== row.stratum) throw new RangeError(`checkMeasures: the key ${JSON.stringify(row.key)} has two strata.`);
|
|
1350
|
+
strata.set(row.key, row.stratum);
|
|
1351
|
+
const measure = measures.get(row.measure) ?? /* @__PURE__ */ new Map();
|
|
1352
|
+
measures.set(row.measure, measure);
|
|
1353
|
+
const groups = measure.get(row.stratum) ?? /* @__PURE__ */ new Map();
|
|
1354
|
+
measure.set(row.stratum, groups);
|
|
1355
|
+
const keys = groups.get(row.group) ?? /* @__PURE__ */ new Map();
|
|
1356
|
+
groups.set(row.group, keys);
|
|
1357
|
+
if (keys.has(row.key)) throw new RangeError(`checkMeasures: the row ${where} comes twice. State how the repeats reduce to one row.`);
|
|
1358
|
+
keys.set(row.key, row);
|
|
1359
|
+
}
|
|
1360
|
+
return measures;
|
|
1361
|
+
}
|
|
1362
|
+
/** This function gives the least count of untied keys with the power to pass at `level`. */
|
|
1363
|
+
function pairsNeeded(level, sides) {
|
|
1364
|
+
let n = 1;
|
|
1365
|
+
while (signTestPower(n, level, sides) === void 0) n += 1;
|
|
1366
|
+
return n;
|
|
1367
|
+
}
|
|
1368
|
+
/** This function compares two groups on the keys that both finished. A positive difference favours `better`. */
|
|
1369
|
+
function rawTest(better, other, stratum, groups, sense) {
|
|
1370
|
+
const a = groups.get(better);
|
|
1371
|
+
const b = groups.get(other);
|
|
1372
|
+
if (a === void 0 || b === void 0) return void 0;
|
|
1373
|
+
const differences = [];
|
|
1374
|
+
for (const [key, row] of a) {
|
|
1375
|
+
const pair = b.get(key);
|
|
1376
|
+
if (pair === void 0 || row.censored === true || pair.censored === true) continue;
|
|
1377
|
+
differences.push(sense === "higher" ? row.value - pair.value : pair.value - row.value);
|
|
1378
|
+
}
|
|
1379
|
+
if (differences.length === 0) return void 0;
|
|
1380
|
+
const wins = differences.filter((d) => d > 0).length;
|
|
1381
|
+
const losses = differences.filter((d) => d < 0).length;
|
|
1382
|
+
return {
|
|
1383
|
+
better,
|
|
1384
|
+
other,
|
|
1385
|
+
stratum,
|
|
1386
|
+
shared: differences.length,
|
|
1387
|
+
wins,
|
|
1388
|
+
losses,
|
|
1389
|
+
medianDifference: median(differences)
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
/** This function corrects one family of tests, over all strata. A test with too few keys beats nothing, even when a step of Holm passes it. */
|
|
1393
|
+
function corrected(tests, sides, alpha, resolution) {
|
|
1394
|
+
if (tests.length === 0) return [];
|
|
1395
|
+
const family = tests.length;
|
|
1396
|
+
const needs = pairsNeeded(alpha / family, sides);
|
|
1397
|
+
const ps = tests.map((t) => signTest(t.wins, t.losses, sides));
|
|
1398
|
+
const steps = holm(ps, alpha);
|
|
1399
|
+
return tests.map((t, i) => {
|
|
1400
|
+
const n = t.wins + t.losses;
|
|
1401
|
+
const tooFew = n < needs;
|
|
1402
|
+
const size = sides === 1 ? t.medianDifference : Math.abs(t.medianDifference);
|
|
1403
|
+
const clears = resolution === void 0 || size > resolution;
|
|
1404
|
+
return {
|
|
1405
|
+
...t,
|
|
1406
|
+
n,
|
|
1407
|
+
ties: t.shared - n,
|
|
1408
|
+
p: ps[i],
|
|
1409
|
+
adjusted: steps[i]?.adjusted,
|
|
1410
|
+
family,
|
|
1411
|
+
needs,
|
|
1412
|
+
tooFew,
|
|
1413
|
+
tied: tooFew && t.shared >= needs,
|
|
1414
|
+
beats: !tooFew && steps[i]?.reject === true && clears
|
|
1415
|
+
};
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
const stratumText = (stratum) => stratum === void 0 ? "" : ` in the stratum ${JSON.stringify(stratum)}`;
|
|
1419
|
+
/** This function tells that a test has too few keys to read, so a tie on enough keys is a read. */
|
|
1420
|
+
const unread = (c) => c.tooFew && !c.tied;
|
|
1421
|
+
/** The reads of one control on one measure: one for each stratum of the measure. */
|
|
1422
|
+
function controlReads(measure, control, data, rule, real, alpha) {
|
|
1423
|
+
if (data === void 0) return [];
|
|
1424
|
+
const comparisons = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry) => rawTest(entry, control, stratum, groups, rule.sense) ?? [])), 1, alpha, rule.resolution);
|
|
1425
|
+
const attacks = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry) => rawTest(control, entry, stratum, groups, rule.sense) ?? [])), 1, alpha, rule.resolution);
|
|
1426
|
+
return [...data.keys()].map((stratum) => {
|
|
1427
|
+
const here = comparisons.filter((c) => c.stratum === stratum);
|
|
1428
|
+
const against = attacks.filter((c) => c.stratum === stratum);
|
|
1429
|
+
return {
|
|
1430
|
+
measure,
|
|
1431
|
+
control,
|
|
1432
|
+
stratum,
|
|
1433
|
+
status: here.length === 0 ? "no-rows" : here.some((c) => c.beats) ? "beaten" : here.every(unread) ? "too-few" : "wins",
|
|
1434
|
+
aheadOf: here.filter((c) => c.losses > c.wins).length,
|
|
1435
|
+
comparisons: here,
|
|
1436
|
+
beatsEntries: against.filter((c) => c.beats).length,
|
|
1437
|
+
of: against.filter((c) => !unread(c)).length,
|
|
1438
|
+
against
|
|
1439
|
+
};
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
/** The read of the spread of one measure: one family over all pairs of real entries and all strata. */
|
|
1443
|
+
function spreadReads(data, rule, real, alpha) {
|
|
1444
|
+
const comparisons = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry, i) => real.slice(i + 1).flatMap((other) => rawTest(entry, other, stratum, groups, rule.sense) ?? []))), 2, alpha, rule.resolution);
|
|
1445
|
+
return {
|
|
1446
|
+
reads: [...data.keys()].map((stratum) => {
|
|
1447
|
+
const here = comparisons.filter((c) => c.stratum === stratum);
|
|
1448
|
+
const widest = here.length === 0 ? void 0 : Math.max(...here.map((c) => Math.abs(c.medianDifference)));
|
|
1449
|
+
return {
|
|
1450
|
+
stratum,
|
|
1451
|
+
pairs: here.length,
|
|
1452
|
+
separated: here.filter((c) => c.beats).length,
|
|
1453
|
+
tooFew: here.filter(unread).length,
|
|
1454
|
+
widest
|
|
1455
|
+
};
|
|
1456
|
+
}),
|
|
1457
|
+
comparisons
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
function countsOf(data) {
|
|
1461
|
+
const counts = [];
|
|
1462
|
+
for (const [stratum, groups] of data ?? []) for (const [group, keys] of groups) {
|
|
1463
|
+
const censored = [...keys.values()].filter((row) => row.censored === true).length;
|
|
1464
|
+
counts.push({
|
|
1465
|
+
group,
|
|
1466
|
+
stratum,
|
|
1467
|
+
finished: keys.size - censored,
|
|
1468
|
+
censored
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
return counts;
|
|
1472
|
+
}
|
|
1473
|
+
/** This function gives the methods of a measure. A row with no method counts as one more method beside a named one. */
|
|
1474
|
+
function methodsOf(data) {
|
|
1475
|
+
const methods = /* @__PURE__ */ new Set();
|
|
1476
|
+
for (const groups of data?.values() ?? []) for (const keys of groups.values()) for (const row of keys.values()) methods.add(row.method);
|
|
1477
|
+
return [...methods];
|
|
1478
|
+
}
|
|
1479
|
+
/** This function tells that a test clears a control on one of `partners` partners. One passed partner is enough, so the level divides by their count. */
|
|
1480
|
+
const clearsPartner = (c, partners, alpha) => c.beats && c.adjusted * partners <= alpha;
|
|
1481
|
+
/** This function reads a control on one of `partners` partners. A test with too few shared keys for the divided level has no power, so a miss there is not a read. */
|
|
1482
|
+
const partnerStatus = (read, partners, alpha) => {
|
|
1483
|
+
if (read === void 0 || read.status === "no-rows") return "no-rows";
|
|
1484
|
+
if (read.comparisons.some((c) => clearsPartner(c, partners, alpha))) return "lost";
|
|
1485
|
+
const needs = pairsNeeded(alpha / ((read.comparisons[0]?.family ?? 1) * partners), 1);
|
|
1486
|
+
return read.comparisons.every((c) => c.shared < needs) ? "too-few" : "not-lost";
|
|
1487
|
+
};
|
|
1488
|
+
function readsOf(reading, measure, control) {
|
|
1489
|
+
const memoKey = JSON.stringify([measure, control]);
|
|
1490
|
+
const known = reading.memo.get(memoKey);
|
|
1491
|
+
if (known !== void 0) return known;
|
|
1492
|
+
const rule = reading.rules.get(measure);
|
|
1493
|
+
const reads = reading.mixed.has(measure) ? [] : controlReads(measure, control, reading.data.get(measure), rule, reading.real, reading.alpha);
|
|
1494
|
+
reading.memo.set(memoKey, reads);
|
|
1495
|
+
return reads;
|
|
1496
|
+
}
|
|
1497
|
+
/** The findings of the rows and the controls of one rule that have nothing to read. */
|
|
1498
|
+
function missingFindings(rule, reading) {
|
|
1499
|
+
const findings = [];
|
|
1500
|
+
const hasRows = (measure, group) => [...reading.data.get(measure)?.values() ?? []].some((groups) => groups.has(group));
|
|
1501
|
+
const missing = (text, control) => {
|
|
1502
|
+
findings.push({
|
|
1503
|
+
kind: "no-rows",
|
|
1504
|
+
measure: rule.measure,
|
|
1505
|
+
...control === void 0 ? {} : { control },
|
|
1506
|
+
text,
|
|
1507
|
+
comparisons: []
|
|
1508
|
+
});
|
|
1509
|
+
};
|
|
1510
|
+
if (!reading.data.has(rule.measure)) missing(`The measure ${rule.measure} has a rule and no rows.`);
|
|
1511
|
+
for (const control of rule.controls) {
|
|
1512
|
+
if (reading.data.has(rule.measure) && !hasRows(rule.measure, control)) missing(`The control ${control} has no rows on ${rule.measure}, so it reads as nothing and not as "not beaten".`, control);
|
|
1513
|
+
for (const partner of rule.partners) if (!hasRows(partner, control)) missing(`The control ${control} has no rows on the partner ${partner} of ${rule.measure}, so it reads as nothing and not as "not beaten".`, control);
|
|
1514
|
+
}
|
|
1515
|
+
return findings;
|
|
1516
|
+
}
|
|
1517
|
+
/** The findings of the win of a control on one stratum: the partners and the censored keys. */
|
|
1518
|
+
function winFindings(rule, read, guard, counts, partnerReads) {
|
|
1519
|
+
const findings = [];
|
|
1520
|
+
const where = stratumText(read.stratum);
|
|
1521
|
+
const count = guard.partners.length;
|
|
1522
|
+
const thin = guard.partners.filter((p) => p.status === "too-few").map((p) => p.partner);
|
|
1523
|
+
const noRows = guard.partners.some((p) => p.status === "no-rows");
|
|
1524
|
+
const behind = partnerReads.flatMap((r) => r.comparisons);
|
|
1525
|
+
if (!guard.partners.some((p) => p.status === "lost")) {
|
|
1526
|
+
if (thin.length > 0) findings.push({
|
|
1527
|
+
kind: "too-few",
|
|
1528
|
+
measure: rule.measure,
|
|
1529
|
+
stratum: read.stratum,
|
|
1530
|
+
control: read.control,
|
|
1531
|
+
partners: count,
|
|
1532
|
+
text: `The control ${read.control} wins ${rule.measure}${where} and loses no partner. Of its ${count} partners, ${thin.join(", ")} have too few keys for a read.`,
|
|
1533
|
+
comparisons: behind
|
|
1534
|
+
});
|
|
1535
|
+
else if (!noRows) {
|
|
1536
|
+
const list = count === 0 ? "It has no partner." : `It loses none of its ${count} partners, ${guard.partners.map((p) => p.partner).join(", ")}.`;
|
|
1537
|
+
findings.push({
|
|
1538
|
+
kind: "uncaught",
|
|
1539
|
+
measure: rule.measure,
|
|
1540
|
+
stratum: read.stratum,
|
|
1541
|
+
control: read.control,
|
|
1542
|
+
partners: count,
|
|
1543
|
+
text: `The control ${read.control} wins ${rule.measure}${where}. ${list}`,
|
|
1544
|
+
comparisons: [...read.comparisons, ...behind]
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
const own = counts.find((c) => c.group === read.control && c.stratum === read.stratum)?.censored ?? 0;
|
|
1549
|
+
const fewer = counts.filter((c) => c.group !== read.control && c.stratum === read.stratum && read.comparisons.some((t) => t.better === c.group) && c.censored < own);
|
|
1550
|
+
if (fewer.length > 0) {
|
|
1551
|
+
const list = fewer.map((c) => `${c.group} ${c.censored}`).join(", ");
|
|
1552
|
+
findings.push({
|
|
1553
|
+
kind: "censored-win",
|
|
1554
|
+
measure: rule.measure,
|
|
1555
|
+
stratum: read.stratum,
|
|
1556
|
+
control: read.control,
|
|
1557
|
+
text: `The control ${read.control} wins ${rule.measure}${where} with ${own} censored keys, more than ${list}. Its win can come from the easy keys.`,
|
|
1558
|
+
comparisons: read.comparisons.filter((t) => fewer.some((c) => c.group === t.better))
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
return findings;
|
|
1562
|
+
}
|
|
1563
|
+
/** The read of one rule, with its findings. */
|
|
1564
|
+
function checkRule(rule, reading, asOf) {
|
|
1565
|
+
const data = reading.data.get(rule.measure);
|
|
1566
|
+
const methods = methodsOf(data);
|
|
1567
|
+
const mixed = reading.mixed.has(rule.measure);
|
|
1568
|
+
const counts = countsOf(data);
|
|
1569
|
+
const findings = missingFindings(rule, reading);
|
|
1570
|
+
const reads = [];
|
|
1571
|
+
const guards = [];
|
|
1572
|
+
if (mixed) findings.push({
|
|
1573
|
+
kind: "mixed-method",
|
|
1574
|
+
measure: rule.measure,
|
|
1575
|
+
text: `The measure ${rule.measure} mixes the methods ${methods.map((m) => m ?? "(none)").join(", ")}. The check has no read of it until the rows are split or marked.`,
|
|
1576
|
+
comparisons: []
|
|
1577
|
+
});
|
|
1578
|
+
if (rule.controls.length === 0) {
|
|
1579
|
+
if (rule.noControl === void 0) findings.push({
|
|
1580
|
+
kind: "no-control",
|
|
1581
|
+
measure: rule.measure,
|
|
1582
|
+
text: `The measure ${rule.measure} has no control and no noControl with a date.`,
|
|
1583
|
+
comparisons: []
|
|
1584
|
+
});
|
|
1585
|
+
else if (rule.noControl.until < asOf) findings.push({
|
|
1586
|
+
kind: "control-late",
|
|
1587
|
+
measure: rule.measure,
|
|
1588
|
+
text: `The control of ${rule.measure} was due on ${rule.noControl.until}, and the date of the check is ${asOf}.`,
|
|
1589
|
+
comparisons: []
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
for (const control of rule.controls) for (const read of readsOf(reading, rule.measure, control)) {
|
|
1593
|
+
const partnerReads = rule.partners.map((partner) => readsOf(reading, partner, control).find((r) => r.stratum === read.stratum));
|
|
1594
|
+
const partners = rule.partners.map((partner, i) => ({
|
|
1595
|
+
partner,
|
|
1596
|
+
status: partnerStatus(partnerReads[i], rule.partners.length, reading.alpha)
|
|
1597
|
+
}));
|
|
1598
|
+
const guard = {
|
|
1599
|
+
control,
|
|
1600
|
+
stratum: read.stratum,
|
|
1601
|
+
status: read.status,
|
|
1602
|
+
partners
|
|
1603
|
+
};
|
|
1604
|
+
guards.push(guard);
|
|
1605
|
+
reads.push(read, ...partnerReads.filter((r) => r !== void 0));
|
|
1606
|
+
if (read.status === "wins") findings.push(...winFindings(rule, read, guard, counts, partnerReads.filter((r) => r !== void 0)));
|
|
1607
|
+
}
|
|
1608
|
+
const controlComparisons = guards.length === 0 ? [] : reads.filter((r) => r.measure === rule.measure).flatMap((r) => r.comparisons);
|
|
1609
|
+
if (rule.controls.length > 0 && guards.length > 0 && !mixed) {
|
|
1610
|
+
if (guards.every((g) => g.status === "too-few" || g.status === "no-rows")) {
|
|
1611
|
+
const needs = Math.max(0, ...controlComparisons.map((c) => c.needs));
|
|
1612
|
+
findings.push({
|
|
1613
|
+
kind: "too-few",
|
|
1614
|
+
measure: rule.measure,
|
|
1615
|
+
text: `No control of ${rule.measure} has enough pairs for a read. A test needs ${needs} untied pairs.`,
|
|
1616
|
+
comparisons: controlComparisons
|
|
1617
|
+
});
|
|
1618
|
+
} else for (const control of rule.controls) {
|
|
1619
|
+
const own = reads.filter((r) => r.measure === rule.measure && r.control === control);
|
|
1620
|
+
const of = own.reduce((sum, r) => sum + r.of, 0);
|
|
1621
|
+
if (of > 0 && own.every((r) => r.beatsEntries === 0)) findings.push({
|
|
1622
|
+
kind: "weak-control",
|
|
1623
|
+
measure: rule.measure,
|
|
1624
|
+
control,
|
|
1625
|
+
text: `The control ${control} beats 0 of ${of} real entries on ${rule.measure} by test, in all strata. The trick buys nothing.`,
|
|
1626
|
+
comparisons: own.flatMap((r) => r.against)
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
let spread = [];
|
|
1631
|
+
if (data !== void 0 && !mixed) {
|
|
1632
|
+
const { reads: spreadRows, comparisons } = spreadReads(data, rule, reading.real, reading.alpha);
|
|
1633
|
+
spread = spreadRows;
|
|
1634
|
+
const testable = comparisons.filter((c) => !unread(c));
|
|
1635
|
+
if (comparisons.length > 0 && testable.length === 0) findings.push({
|
|
1636
|
+
kind: "too-few",
|
|
1637
|
+
measure: rule.measure,
|
|
1638
|
+
text: `No pair of real entries has enough keys for a read of the spread of ${rule.measure}. A test needs ${comparisons[0]?.needs} untied pairs.`,
|
|
1639
|
+
comparisons: []
|
|
1640
|
+
});
|
|
1641
|
+
else if (testable.length > 0 && !comparisons.some((c) => c.beats)) {
|
|
1642
|
+
const widest = Math.max(...comparisons.map((c) => Math.abs(c.medianDifference)));
|
|
1643
|
+
const basis = rule.resolution === void 0 ? "by the test only" : `by the test and the resolution ${rule.resolution}`;
|
|
1644
|
+
findings.push({
|
|
1645
|
+
kind: "no-spread",
|
|
1646
|
+
measure: rule.measure,
|
|
1647
|
+
text: `No pair of the ${comparisons.length} pairs of real entries separates on ${rule.measure}, ${basis}. The widest median difference is ${widest}.`,
|
|
1648
|
+
comparisons: [...comparisons].sort((a, b) => a.adjusted - b.adjusted).slice(0, 1)
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return {
|
|
1653
|
+
check: {
|
|
1654
|
+
measure: rule.measure,
|
|
1655
|
+
sense: rule.sense,
|
|
1656
|
+
basis: rule.resolution === void 0 ? "test only" : "test and resolution",
|
|
1657
|
+
method: mixed ? void 0 : methods[0],
|
|
1658
|
+
mixed,
|
|
1659
|
+
guards,
|
|
1660
|
+
spread,
|
|
1661
|
+
entries: counts,
|
|
1662
|
+
partners: rule.partners,
|
|
1663
|
+
open: rule.noControl
|
|
1664
|
+
},
|
|
1665
|
+
findings,
|
|
1666
|
+
reads
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* This function reads the controls and the spread of each measure of a bench.
|
|
1671
|
+
*
|
|
1672
|
+
* - A control wins a measure when no real entry beats it on the keys that both finished. A tie counts for the control.
|
|
1673
|
+
* - A real entry beats a control when the Holm-corrected sign test passes and the median paired difference is over the resolution.
|
|
1674
|
+
* - One Holm family holds the tests of one control on one measure over all real entries and all strata.
|
|
1675
|
+
* - One more Holm family holds the tests of one control against the real entries, with the same need and resolution. A control that beats none of them is weak.
|
|
1676
|
+
* - One Holm family holds the tests of the spread of one measure over all pairs of real entries and all strata.
|
|
1677
|
+
* - A test with fewer untied keys than `signTestPower` needs for that family reads `tooFew` and beats nothing.
|
|
1678
|
+
* - A test with enough shared keys and too few untied keys is `tied`. The two entries read as equal, so a constant measure is `no-spread` and not `too-few`.
|
|
1679
|
+
* - A control loses a partner only at the level divided by the count of its partners. One passed partner clears `uncaught`, so each partner is one more chance.
|
|
1680
|
+
*
|
|
1681
|
+
* The function throws a `RangeError` for a value that is not finite, a repeat of measure, group and key, or a key in two strata.
|
|
1682
|
+
* It also throws one for rules that contradict each other.
|
|
1683
|
+
* The rules, the controls and the date are stated, so the result depends only on the arguments.
|
|
1684
|
+
*/
|
|
1685
|
+
function checkMeasures(rows, options) {
|
|
1686
|
+
const alpha = options.alpha ?? DEFAULT_ALPHA;
|
|
1687
|
+
if (!(alpha > 0 && alpha < 1)) throw new RangeError(`checkMeasures: alpha is over 0 and under 1, not ${alpha}.`);
|
|
1688
|
+
const asOf = checkedDate("asOf", options.asOf);
|
|
1689
|
+
const rules = checkedRules(options);
|
|
1690
|
+
const data = groupedRows(rows);
|
|
1691
|
+
const controls = new Set(options.controls);
|
|
1692
|
+
const reading = {
|
|
1693
|
+
rules,
|
|
1694
|
+
data,
|
|
1695
|
+
real: [...new Set(rows.map((row) => row.group))].filter((group) => !controls.has(group)),
|
|
1696
|
+
alpha,
|
|
1697
|
+
mixed: new Set([...data.keys()].filter((measure) => methodsOf(data.get(measure)).length > 1)),
|
|
1698
|
+
memo: /* @__PURE__ */ new Map()
|
|
1699
|
+
};
|
|
1700
|
+
const findings = [];
|
|
1701
|
+
for (const measure of data.keys()) if (!rules.has(measure)) findings.push({
|
|
1702
|
+
kind: "no-rule",
|
|
1703
|
+
measure,
|
|
1704
|
+
text: `The measure ${measure} has rows and no rule.`,
|
|
1705
|
+
comparisons: []
|
|
1706
|
+
});
|
|
1707
|
+
const measures = [];
|
|
1708
|
+
const reads = /* @__PURE__ */ new Map();
|
|
1709
|
+
for (const rule of options.rules) {
|
|
1710
|
+
const result = checkRule(rule, reading, asOf);
|
|
1711
|
+
measures.push(result.check);
|
|
1712
|
+
findings.push(...result.findings);
|
|
1713
|
+
for (const read of result.reads) reads.set(JSON.stringify([
|
|
1714
|
+
read.measure,
|
|
1715
|
+
read.control,
|
|
1716
|
+
read.stratum ?? null
|
|
1717
|
+
]), read);
|
|
1718
|
+
}
|
|
1719
|
+
return {
|
|
1720
|
+
alpha,
|
|
1721
|
+
measures,
|
|
1722
|
+
reads: [...reads.values()],
|
|
1723
|
+
findings
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* This function tells what a page says about the win of one real entry on one measure.
|
|
1728
|
+
* The claim is about this entry: only this entry beating the control, on this measure or on a partner, clears the entry. A win by another entry clears nothing.
|
|
1729
|
+
* With a stratum, the function reads that stratum. Without one, it reads the rows with no stratum.
|
|
1730
|
+
* The function throws a `RangeError` for a measure with no read and for an entry that the rows do not hold.
|
|
1731
|
+
*/
|
|
1732
|
+
function winClaim(check, measure, entry, stratum) {
|
|
1733
|
+
const read = check.measures.find((m) => m.measure === measure);
|
|
1734
|
+
if (read === void 0) throw new RangeError(`winClaim: the measure ${JSON.stringify(measure)} has no rule.`);
|
|
1735
|
+
const count = read.entries.find((c) => c.group === entry && c.stratum === stratum);
|
|
1736
|
+
if (count === void 0) throw new RangeError(`winClaim: the entry ${JSON.stringify(entry)} has no rows on ${JSON.stringify(measure)}${stratumText(stratum)}.`);
|
|
1737
|
+
const guards = read.guards.filter((g) => g.stratum === stratum);
|
|
1738
|
+
const winning = guards.filter((g) => g.status === "wins");
|
|
1739
|
+
const lostByControl = (guard) => guard.partners.filter((p) => p.status === "lost").map((p) => p.partner);
|
|
1740
|
+
const testsOf = (of, control) => check.reads.find((r) => r.measure === of && r.control === control && r.stratum === stratum)?.comparisons ?? [];
|
|
1741
|
+
const notBeaten = guards.filter((g) => !testsOf(measure, g.control).some((c) => c.better === entry && c.beats));
|
|
1742
|
+
const clearsOnPartner = (guard) => guard.partners.some((p) => testsOf(p.partner, guard.control).some((c) => c.better === entry && clearsPartner(c, guard.partners.length, check.alpha)));
|
|
1743
|
+
const countOf = (control) => read.entries.find((c) => c.group === control && c.stratum === stratum)?.censored ?? 0;
|
|
1744
|
+
const alone = guards.length > 0 && notBeaten.length === 0;
|
|
1745
|
+
return {
|
|
1746
|
+
alone,
|
|
1747
|
+
show: notBeaten.length > 0 && notBeaten.every((g) => g.status === "wins") ? [...new Set(winning.flatMap(lostByControl))] : alone ? [] : read.partners,
|
|
1748
|
+
likeControl: notBeaten.some((guard) => !clearsOnPartner(guard)),
|
|
1749
|
+
censored: {
|
|
1750
|
+
entry: count.censored,
|
|
1751
|
+
controls: notBeaten.map((g) => ({
|
|
1752
|
+
control: g.control,
|
|
1753
|
+
censored: countOf(g.control)
|
|
1754
|
+
}))
|
|
1755
|
+
},
|
|
1756
|
+
open: read.open
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1092
1759
|
// src/power.ts
|
|
1093
1760
|
/**
|
|
1094
1761
|
* The power test of a time verdict. It draws seeded benches with a known speedup and counts the verdicts that find it.
|
|
@@ -1203,6 +1870,6 @@ function timeRounds(arms, options) {
|
|
|
1203
1870
|
sink
|
|
1204
1871
|
};
|
|
1205
1872
|
}
|
|
1206
|
-
export { BUDGET_LOOKS, LADDER_FORMAT, aaBand, aaSpread, armVerdicts, balanceOrders, budgetLookSpend, budgetLooksVerdict, budgetVerdict, caseIntervals, caseRatios, checkLadder, checkPairedTime, controlFactor, firstArm, ladderVerdict, limitVerdict, poolRecords, pooledBand, pooledLimitVerdict, pooledVerdict, rungVerdict, timeRounds, timeVerdict, verdictMargin, verdictPower };
|
|
1873
|
+
export { BUDGET_LOOKS, GATE_LOOKS, LADDER_FORMAT, aaBand, aaSpread, armVerdicts, balanceOrders, budgetLookSpend, budgetLooksVerdict, budgetVerdict, caseIntervals, caseRatios, checkLadder, checkMeasures, checkPairedTime, controlFactor, firstArm, gateOf, ladderVerdict, limitVerdict, notSlowerVerdict, poolRecords, pooledBand, pooledLimitVerdict, pooledVerdict, rungVerdict, timeRounds, timeVerdict, verdictMargin, verdictPower, winClaim };
|
|
1207
1874
|
|
|
1208
1875
|
//# sourceMappingURL=index.js.map
|