peeksafe 0.1.0 → 0.2.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/CHANGELOG.md +180 -1
- package/README.md +3 -5
- package/dist/cluster.d.ts.map +1 -1
- package/dist/cluster.js +8 -0
- package/dist/cluster.js.map +1 -1
- package/dist/errors.d.ts +24 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +52 -3
- package/dist/errors.js.map +1 -1
- package/dist/frontier.d.ts +5 -3
- package/dist/frontier.d.ts.map +1 -1
- package/dist/frontier.js +78 -33
- package/dist/frontier.js.map +1 -1
- package/dist/gate.d.ts +2 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +5 -1
- package/dist/gate.js.map +1 -1
- package/dist/plan.d.ts +42 -10
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +176 -32
- package/dist/plan.js.map +1 -1
- package/dist/rand.d.ts.map +1 -1
- package/dist/rand.js +16 -4
- package/dist/rand.js.map +1 -1
- package/dist/stats.d.ts +7 -3
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +101 -42
- package/dist/stats.js.map +1 -1
- package/dist/stop.d.ts +1 -0
- package/dist/stop.d.ts.map +1 -1
- package/dist/stop.js +13 -5
- package/dist/stop.js.map +1 -1
- package/package.json +10 -11
- package/src/cluster.ts +12 -0
- package/src/errors.ts +70 -3
- package/src/frontier.ts +102 -47
- package/src/gate.ts +6 -1
- package/src/plan.ts +236 -46
- package/src/rand.ts +20 -4
- package/src/stats.ts +132 -48
- package/src/stop.ts +17 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peeksafe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Statistically valid gates for non-deterministic eval suites. Stop early without losing error control.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"evals",
|
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=22"
|
|
45
45
|
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@biomejs/biome": "2.5.15",
|
|
48
|
+
"@types/node": "22.20.5",
|
|
49
|
+
"knip": "6.39.0",
|
|
50
|
+
"lefthook": "2.1.16",
|
|
51
|
+
"typescript": "7.0.2",
|
|
52
|
+
"vitest": "5.0.3"
|
|
53
|
+
},
|
|
46
54
|
"scripts": {
|
|
47
55
|
"build": "tsc -p tsconfig.build.json",
|
|
48
56
|
"clean": "rm -rf dist",
|
|
@@ -52,15 +60,6 @@
|
|
|
52
60
|
"knip": "knip --treat-config-hints-as-errors",
|
|
53
61
|
"test": "vitest run",
|
|
54
62
|
"test:watch": "vitest",
|
|
55
|
-
"prepack": "pnpm run clean && pnpm run build",
|
|
56
63
|
"version": "node scripts/release-changelog.mjs && git add CHANGELOG.md"
|
|
57
|
-
},
|
|
58
|
-
"devDependencies": {
|
|
59
|
-
"@biomejs/biome": "catalog:",
|
|
60
|
-
"@types/node": "catalog:",
|
|
61
|
-
"knip": "catalog:",
|
|
62
|
-
"lefthook": "catalog:",
|
|
63
|
-
"typescript": "catalog:",
|
|
64
|
-
"vitest": "catalog:"
|
|
65
64
|
}
|
|
66
|
-
}
|
|
65
|
+
}
|
package/src/cluster.ts
CHANGED
|
@@ -188,6 +188,18 @@ export interface ClusteredMean extends Interval {
|
|
|
188
188
|
/** The naive iid interval, kept so the two can be printed side by side. */
|
|
189
189
|
export function iidMean(values: number[], level = 0.95): Interval {
|
|
190
190
|
requireLevel(level, "iidMean");
|
|
191
|
+
// groupByCluster refused these for the other estimators, so the same data was an error there and a NaN interval here.
|
|
192
|
+
for (let i = 0; i < values.length; i++) {
|
|
193
|
+
if (!Number.isFinite(values[i])) {
|
|
194
|
+
throw new PeeksafeError(
|
|
195
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
196
|
+
`iidMean: value at index ${i} is not finite`,
|
|
197
|
+
{
|
|
198
|
+
detail: { index: i, value: values[i] },
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
191
203
|
const n = values.length;
|
|
192
204
|
if (n === 0) return { low: -1, high: 1, point: 0, observations: 0 };
|
|
193
205
|
const mean = values.reduce((a, b) => a + b, 0) / n;
|
package/src/errors.ts
CHANGED
|
@@ -153,11 +153,11 @@ export function requireAltConcentration(v: number, evidence: string, where: stri
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
|
-
* The error rates of
|
|
157
|
-
* between 0 and 1, and summing to less than 1.
|
|
156
|
+
* The error rates of a Wald SPRT, run by `sprtDecision` or priced by a
|
|
157
|
+
* planner: each strictly between 0 and 1, and summing to less than 1.
|
|
158
158
|
*
|
|
159
159
|
* At `alpha + beta >= 1` Wald's walls meet or cross, so the test decides
|
|
160
|
-
* before it has seen a run. `sprtExpectedN`
|
|
160
|
+
* before it has seen a run. `sprtExpectedN` answered a small positive number
|
|
161
161
|
* there, and a plan at `alpha: 0.6, beta: 0.5` priced every case at
|
|
162
162
|
* `minTrials` with nothing to say the design had no error control left.
|
|
163
163
|
*/
|
|
@@ -176,6 +176,59 @@ export function requireErrorRates(alpha: number, beta: number, where: string): v
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* `runsPerCaseCeiling`: finite and at least one run. The planners search up to
|
|
181
|
+
* eight times it, and at NaN or 0 that search never ran, so every case came
|
|
182
|
+
* back undecidable with a reason that blamed the evidence bar.
|
|
183
|
+
*/
|
|
184
|
+
export function requireRunCeiling(v: number, where: string): void {
|
|
185
|
+
if (!(v >= 1) || !Number.isFinite(v)) {
|
|
186
|
+
throw new PeeksafeError(
|
|
187
|
+
"PEEKSAFE_E_CONFIG",
|
|
188
|
+
`${where}: runsPerCaseCeiling must be finite and at least 1, got ${v}`,
|
|
189
|
+
{
|
|
190
|
+
detail: { runsPerCaseCeiling: v, where },
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The batch grid a plan prices the gate on: `minTrials` and `batchSize`
|
|
198
|
+
* positive integers, `maxTrials` an integer at least `minTrials`. A NaN bound
|
|
199
|
+
* or a zero batch made the typical bill NaN, and a cap under the minimum
|
|
200
|
+
* priced every case at the cap.
|
|
201
|
+
*/
|
|
202
|
+
export function requireTrialGrid(
|
|
203
|
+
cfg: { minTrials: number; maxTrials: number; batchSize: number },
|
|
204
|
+
where: string,
|
|
205
|
+
): void {
|
|
206
|
+
const { minTrials, maxTrials, batchSize } = cfg;
|
|
207
|
+
const detail = { minTrials, maxTrials, batchSize, where };
|
|
208
|
+
for (const [name, v] of [
|
|
209
|
+
["minTrials", minTrials],
|
|
210
|
+
["batchSize", batchSize],
|
|
211
|
+
] as const) {
|
|
212
|
+
if (!Number.isInteger(v) || v < 1) {
|
|
213
|
+
throw new PeeksafeError(
|
|
214
|
+
"PEEKSAFE_E_CONFIG",
|
|
215
|
+
`${where}: ${name} must be a positive integer, got ${v}`,
|
|
216
|
+
{ detail },
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!Number.isInteger(maxTrials) || maxTrials < minTrials) {
|
|
221
|
+
throw new PeeksafeError(
|
|
222
|
+
"PEEKSAFE_E_CONFIG",
|
|
223
|
+
`${where}: maxTrials must be an integer ≥ minTrials, got ${maxTrials} vs ${minTrials}`,
|
|
224
|
+
{
|
|
225
|
+
detail,
|
|
226
|
+
hint: "the typical bill is clamped to [minTrials, maxTrials]; a cap under the minimum prices a gate that stops before it starts",
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
179
232
|
/** The four cells of a pair table, each a non-negative integer. */
|
|
180
233
|
export function requirePairedCounts(
|
|
181
234
|
p: { bothPass: number; worse: number; better: number; bothFail: number },
|
|
@@ -206,3 +259,17 @@ export function requireProbability(p: number, name: string, where: string): void
|
|
|
206
259
|
);
|
|
207
260
|
}
|
|
208
261
|
}
|
|
262
|
+
|
|
263
|
+
/** Case ids, each at most once: a repeated id is planned twice and inflates the suite size the bar is set from. */
|
|
264
|
+
export function requireUniqueCaseIds(cases: readonly { id: string }[], where: string): void {
|
|
265
|
+
const seen = new Set<string>();
|
|
266
|
+
for (const c of cases) {
|
|
267
|
+
if (seen.has(c.id)) {
|
|
268
|
+
throw new PeeksafeError("PEEKSAFE_E_CASE_DUPLICATE", `${where}: duplicate case id ${c.id}`, {
|
|
269
|
+
detail: { id: c.id, where },
|
|
270
|
+
hint: "a case listed twice is priced twice and counted twice in the suite size; dedupe the case list before calling",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
seen.add(c.id);
|
|
274
|
+
}
|
|
275
|
+
}
|
package/src/frontier.ts
CHANGED
|
@@ -62,8 +62,10 @@ import {
|
|
|
62
62
|
requireErrorRates,
|
|
63
63
|
requireOpenProbability,
|
|
64
64
|
requireProbability,
|
|
65
|
+
requireRunCeiling,
|
|
66
|
+
requireTrialGrid,
|
|
65
67
|
} from "./errors.js";
|
|
66
|
-
import { DEFAULT_PLAN } from "./plan.js";
|
|
68
|
+
import { DEFAULT_PLAN, expectedSettlePairs, requireSettleConfidence } from "./plan.js";
|
|
67
69
|
import {
|
|
68
70
|
betaQuantile,
|
|
69
71
|
ebhSoloThreshold,
|
|
@@ -161,9 +163,9 @@ export interface FrontierConfig {
|
|
|
161
163
|
/**
|
|
162
164
|
* Baseline runs per case the *paired* design needs. The paired e-value's null
|
|
163
165
|
* is a point mass, so the stored baseline is not part of its validity and
|
|
164
|
-
* does not set a ceiling
|
|
165
|
-
*
|
|
166
|
-
* design wins at these prices.
|
|
166
|
+
* does not set a ceiling; here it is only the baseline bill, since a paired
|
|
167
|
+
* typical bill is priced in pairs to settle. A short one is enough, and that
|
|
168
|
+
* asymmetry is most of why the paired design wins at these prices.
|
|
167
169
|
*/
|
|
168
170
|
pairedBaselineRuns: number;
|
|
169
171
|
/** fraction of cases a screen carries through to the expensive test */
|
|
@@ -185,6 +187,8 @@ export interface FrontierConfig {
|
|
|
185
187
|
maxTrials: number;
|
|
186
188
|
batchSize: number;
|
|
187
189
|
nullQuantile: number;
|
|
190
|
+
/** the `futilityConfidence` `shouldStopPaired` runs at; unset takes its default */
|
|
191
|
+
futilityConfidence?: number;
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
export const DEFAULT_FRONTIER: FrontierConfig = {
|
|
@@ -268,6 +272,7 @@ export interface FrontierPoint {
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
const RUNS_PER_OBS: Record<FrontierDesign, number> = { unpaired: 1, paired: 2 };
|
|
275
|
+
const SCREENS: readonly FrontierScreen[] = ["none", "expensive", "proxy"];
|
|
271
276
|
|
|
272
277
|
/** $1.00, but $0.0021 rather than $0.00, a per-run price rounds to nothing. */
|
|
273
278
|
export const fmtRate = (x: number): string =>
|
|
@@ -283,6 +288,62 @@ export const fmtRate = (x: number): string =>
|
|
|
283
288
|
const graded = (runs: number, cfg: FrontierConfig): number =>
|
|
284
289
|
runs * (cfg.costPerRunUsd + cfg.costPerGradeUsd);
|
|
285
290
|
|
|
291
|
+
function requireBaselineRuns(v: number, name: string, where: string): void {
|
|
292
|
+
if (!Number.isInteger(v) || v < 1) {
|
|
293
|
+
throw new PeeksafeError(
|
|
294
|
+
"PEEKSAFE_E_CONFIG",
|
|
295
|
+
`${where}: ${name} must be a positive integer, got ${v}`,
|
|
296
|
+
{
|
|
297
|
+
detail: { [name]: v, where },
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The budget, the prices, the screen's keep fraction and `amortisePrs`.
|
|
305
|
+
* `enumerateFrontier` checked these and `evaluatePoint` did not, so a direct
|
|
306
|
+
* call recommended a point at a negative bill and priced a screen that kept
|
|
307
|
+
* twice the suite.
|
|
308
|
+
*/
|
|
309
|
+
function requirePricing(cfg: FrontierConfig, where: string): void {
|
|
310
|
+
const finiteCost = (v: number) => Number.isFinite(v) && v >= 0;
|
|
311
|
+
if (!(cfg.budgetUsd >= 0) || !finiteCost(cfg.costPerRunUsd) || !finiteCost(cfg.costPerGradeUsd)) {
|
|
312
|
+
throw new PeeksafeError(
|
|
313
|
+
"PEEKSAFE_E_CONFIG",
|
|
314
|
+
`${where}: budget must be ≥ 0 and costs finite and ≥ 0`,
|
|
315
|
+
{
|
|
316
|
+
detail: {
|
|
317
|
+
budgetUsd: cfg.budgetUsd,
|
|
318
|
+
costPerRunUsd: cfg.costPerRunUsd,
|
|
319
|
+
costPerGradeUsd: cfg.costPerGradeUsd,
|
|
320
|
+
where,
|
|
321
|
+
},
|
|
322
|
+
hint: "an infinite cost fits an infinite budget, so the frontier recommended a point on an infinite bill",
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (!(cfg.screenKeepFraction > 0) || !(cfg.screenKeepFraction <= 1)) {
|
|
327
|
+
throw new PeeksafeError(
|
|
328
|
+
"PEEKSAFE_E_CONFIG",
|
|
329
|
+
`${where}: screenKeepFraction must be in (0,1], got ${cfg.screenKeepFraction}`,
|
|
330
|
+
{
|
|
331
|
+
detail: { screenKeepFraction: cfg.screenKeepFraction, where },
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (!Number.isInteger(cfg.amortisePrs) || cfg.amortisePrs < 1) {
|
|
336
|
+
throw new PeeksafeError(
|
|
337
|
+
"PEEKSAFE_E_CONFIG",
|
|
338
|
+
`${where}: amortisePrs must be a positive integer, got ${cfg.amortisePrs}`,
|
|
339
|
+
{
|
|
340
|
+
detail: { amortisePrs: cfg.amortisePrs, where },
|
|
341
|
+
hint: 'a baseline spread over "infinity" pull requests is how the baseline cost disappears from a plan',
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
286
347
|
/**
|
|
287
348
|
* Observations per case on a pull request where nothing moved, clamped onto the
|
|
288
349
|
* gate's own batch grid.
|
|
@@ -298,6 +359,11 @@ export function typicalObservationsPerCase(
|
|
|
298
359
|
// 0 for a quantile at or below 0, 1 at or above 1, and about 0 for NaN, so the
|
|
299
360
|
// frontier priced the typical bill against a null rate nobody chose.
|
|
300
361
|
requireOpenProbability(cfg.nullQuantile, "nullQuantile", "typicalObservationsPerCase");
|
|
362
|
+
requireTrialGrid(cfg, "typicalObservationsPerCase");
|
|
363
|
+
// Unchecked, an mde of 0 or below priced a bill for an effect nobody can detect, and a bad rate or run count was refused under betaQuantile's name.
|
|
364
|
+
requireOpenProbability(rate, "rate", "typicalObservationsPerCase");
|
|
365
|
+
requireOpenProbability(mde, "mde", "typicalObservationsPerCase");
|
|
366
|
+
requireBaselineRuns(baselineRuns, "baselineRuns", "typicalObservationsPerCase");
|
|
301
367
|
const s = Math.round(rate * baselineRuns);
|
|
302
368
|
const p0 = betaQuantile(1 + s, 1 + baselineRuns - s, cfg.nullQuantile);
|
|
303
369
|
const p1 = Math.max(0.005, p0 - mde);
|
|
@@ -334,6 +400,11 @@ export function evaluatePoint(
|
|
|
334
400
|
requireErrorRates(cfg.alpha, cfg.beta, "evaluatePoint");
|
|
335
401
|
requireProbability(cfg.pairCoupling, "pairCoupling", "evaluatePoint");
|
|
336
402
|
requireOpenProbability(cfg.nullQuantile, "nullQuantile", "evaluatePoint");
|
|
403
|
+
requireSettleConfidence(cfg, "evaluatePoint");
|
|
404
|
+
// makePlan refuses it; unchecked, an fdr of 2 cut the bar and 0 was blamed on the ceiling.
|
|
405
|
+
requireOpenProbability(cfg.fdr, "fdr", "evaluatePoint");
|
|
406
|
+
requireRunCeiling(cfg.runsPerCaseCeiling, "evaluatePoint");
|
|
407
|
+
requirePricing(cfg, "evaluatePoint");
|
|
337
408
|
if (!Number.isInteger(cases) || cases < 1) {
|
|
338
409
|
throw new PeeksafeError(
|
|
339
410
|
"PEEKSAFE_E_CONFIG",
|
|
@@ -343,25 +414,35 @@ export function evaluatePoint(
|
|
|
343
414
|
},
|
|
344
415
|
);
|
|
345
416
|
}
|
|
346
|
-
|
|
417
|
+
// The paired design prices its baseline from this one, and 0 or 2.5 runs priced a cheaper frontier than any real baseline.
|
|
418
|
+
requireBaselineRuns(cfg.pairedBaselineRuns, "pairedBaselineRuns", "evaluatePoint");
|
|
419
|
+
requireBaselineRuns(baselineRuns, "baselineRuns", "evaluatePoint");
|
|
420
|
+
requireTrialGrid(cfg, "evaluatePoint");
|
|
421
|
+
// An unknown design fell through to the paired branch with no runs per observation and came back feasible on an infinite bill.
|
|
422
|
+
if (!Object.hasOwn(RUNS_PER_OBS, design)) {
|
|
347
423
|
throw new PeeksafeError(
|
|
348
424
|
"PEEKSAFE_E_CONFIG",
|
|
349
|
-
`frontier:
|
|
425
|
+
`frontier: design must be one of ${Object.keys(RUNS_PER_OBS).join(", ")}, got "${String(design)}"`,
|
|
350
426
|
{
|
|
351
|
-
detail: {
|
|
427
|
+
detail: { design },
|
|
352
428
|
},
|
|
353
429
|
);
|
|
354
430
|
}
|
|
355
|
-
|
|
431
|
+
// An unknown screen was priced and recommended as the expensive one.
|
|
432
|
+
if (!SCREENS.includes(screen)) {
|
|
356
433
|
throw new PeeksafeError(
|
|
357
434
|
"PEEKSAFE_E_CONFIG",
|
|
358
|
-
`frontier:
|
|
435
|
+
`frontier: screen must be one of ${SCREENS.join(", ")}, got "${String(screen)}"`,
|
|
359
436
|
{
|
|
360
|
-
detail: {
|
|
361
|
-
hint: "the ceiling bill is priced at this cap; a cap of zero prices a gate that never runs",
|
|
437
|
+
detail: { screen },
|
|
362
438
|
},
|
|
363
439
|
);
|
|
364
440
|
}
|
|
441
|
+
// Only null withholds a recommendation, so a NaN recall (0 of 0 regressions caught) recommended the screen.
|
|
442
|
+
for (const kind of ["expensive", "proxy"] as const) {
|
|
443
|
+
const recall = cfg.screenRecall[kind];
|
|
444
|
+
if (recall !== null) requireProbability(recall, `screenRecall.${kind}`, "evaluatePoint");
|
|
445
|
+
}
|
|
365
446
|
if (screen !== "none" && (!Number.isInteger(screenRunsPerCase) || screenRunsPerCase < 1)) {
|
|
366
447
|
throw new PeeksafeError(
|
|
367
448
|
"PEEKSAFE_E_CONFIG",
|
|
@@ -428,7 +509,11 @@ export function evaluatePoint(
|
|
|
428
509
|
}
|
|
429
510
|
const feasible = infeasibleBecause === null;
|
|
430
511
|
|
|
431
|
-
|
|
512
|
+
// A paired case counts the pairs `shouldStopPaired` takes to settle, as `makePlan` does; the SPRT figure ignores the coupling.
|
|
513
|
+
const typicalObservations =
|
|
514
|
+
design === "paired"
|
|
515
|
+
? expectedSettlePairs(rate, cfg.pairCoupling, mde, cfg)
|
|
516
|
+
: typicalObservationsPerCase(rate, mde, cfg, effectiveBaselineRuns);
|
|
432
517
|
const keep = screen === "none" ? cases : Math.max(1, Math.ceil(cases * cfg.screenKeepFraction));
|
|
433
518
|
|
|
434
519
|
// Screening runs are graded by whichever grader the screen uses. A proxy
|
|
@@ -449,7 +534,7 @@ export function evaluatePoint(
|
|
|
449
534
|
|
|
450
535
|
// The baseline. Unpaired buys its ceiling here; paired does not.
|
|
451
536
|
const baselineRunsTotal = cases * effectiveBaselineRuns;
|
|
452
|
-
const baselineRunsAmortised = baselineRunsTotal /
|
|
537
|
+
const baselineRunsAmortised = baselineRunsTotal / cfg.amortisePrs;
|
|
453
538
|
const baselineUsd = graded(baselineRunsAmortised, cfg);
|
|
454
539
|
|
|
455
540
|
const costFor = (obsPerCase: number): FrontierCost => {
|
|
@@ -495,8 +580,8 @@ export function evaluatePoint(
|
|
|
495
580
|
"certify-all": certifyAll,
|
|
496
581
|
ceiling,
|
|
497
582
|
};
|
|
498
|
-
|
|
499
|
-
if (
|
|
583
|
+
// `hasOwn`, as in `costShares`: `BILLS.toString` is inherited, so "toString" passed an `undefined` check.
|
|
584
|
+
if (!Object.hasOwn(BILLS, cfg.basis)) {
|
|
500
585
|
throw new PeeksafeError(
|
|
501
586
|
"PEEKSAFE_E_CONFIG",
|
|
502
587
|
`frontier: basis must be one of ${Object.keys(BILLS).join(", ")}, got "${String(cfg.basis)}"`,
|
|
@@ -506,6 +591,7 @@ export function evaluatePoint(
|
|
|
506
591
|
},
|
|
507
592
|
);
|
|
508
593
|
}
|
|
594
|
+
const basisCost = BILLS[cfg.basis];
|
|
509
595
|
const withinBudget = feasible && basisCost.totalUsd <= cfg.budgetUsd;
|
|
510
596
|
|
|
511
597
|
// Reportable and recommendable are different. A screen whose recall nobody
|
|
@@ -551,38 +637,7 @@ export function enumerateFrontier(
|
|
|
551
637
|
cfg: FrontierConfig,
|
|
552
638
|
axes: FrontierAxes = DEFAULT_AXES,
|
|
553
639
|
): FrontierPoint[] {
|
|
554
|
-
|
|
555
|
-
throw new PeeksafeError(
|
|
556
|
-
"PEEKSAFE_E_CONFIG",
|
|
557
|
-
"frontier: budget and costs must be ≥ 0 and not NaN",
|
|
558
|
-
{
|
|
559
|
-
detail: {
|
|
560
|
-
budgetUsd: cfg.budgetUsd,
|
|
561
|
-
costPerRunUsd: cfg.costPerRunUsd,
|
|
562
|
-
costPerGradeUsd: cfg.costPerGradeUsd,
|
|
563
|
-
},
|
|
564
|
-
},
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
if (!(cfg.screenKeepFraction > 0) || !(cfg.screenKeepFraction <= 1)) {
|
|
568
|
-
throw new PeeksafeError(
|
|
569
|
-
"PEEKSAFE_E_CONFIG",
|
|
570
|
-
`frontier: screenKeepFraction must be in (0,1], got ${cfg.screenKeepFraction}`,
|
|
571
|
-
{
|
|
572
|
-
detail: { screenKeepFraction: cfg.screenKeepFraction },
|
|
573
|
-
},
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
if (!Number.isInteger(cfg.amortisePrs) || cfg.amortisePrs < 1) {
|
|
577
|
-
throw new PeeksafeError(
|
|
578
|
-
"PEEKSAFE_E_CONFIG",
|
|
579
|
-
`frontier: amortisePrs must be a positive integer, got ${cfg.amortisePrs}`,
|
|
580
|
-
{
|
|
581
|
-
detail: { amortisePrs: cfg.amortisePrs },
|
|
582
|
-
hint: 'a baseline spread over "infinity" pull requests is how the baseline cost disappears from a plan',
|
|
583
|
-
},
|
|
584
|
-
);
|
|
585
|
-
}
|
|
640
|
+
requirePricing(cfg, "frontier");
|
|
586
641
|
for (const [name, xs] of Object.entries(axes) as Array<[string, unknown[]]>) {
|
|
587
642
|
if (!Array.isArray(xs) || xs.length === 0) {
|
|
588
643
|
throw new PeeksafeError(
|
package/src/gate.ts
CHANGED
|
@@ -80,6 +80,11 @@ export const cannotDropBy = (
|
|
|
80
80
|
mde: number,
|
|
81
81
|
): boolean => baseline.successes / baseline.trials <= mde;
|
|
82
82
|
|
|
83
|
+
/** `options` without its `undefined` entries, so a spread over the defaults leaves those defaults in place. */
|
|
84
|
+
export function definedOptions<T extends object>(options: T): T {
|
|
85
|
+
return Object.fromEntries(Object.entries(options ?? {}).filter(([, v]) => v !== undefined)) as T;
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
/** One case's candidate result, and the baseline it is measured against. */
|
|
84
89
|
export interface GateCase {
|
|
85
90
|
id: string;
|
|
@@ -195,7 +200,7 @@ export function gate(
|
|
|
195
200
|
cases: readonly (GateCase | PairedGateCase)[],
|
|
196
201
|
options: GateOptions = {},
|
|
197
202
|
): GateResult {
|
|
198
|
-
const opts: Required<GateOptions> = { ...DEFAULT_GATE_OPTIONS, ...options };
|
|
203
|
+
const opts: Required<GateOptions> = { ...DEFAULT_GATE_OPTIONS, ...definedOptions(options) };
|
|
199
204
|
requireOpenProbability(opts.mde, "mde", "gate");
|
|
200
205
|
requireOpenProbability(opts.fdr, "fdr", "gate");
|
|
201
206
|
requireEvidence(opts.evidence, "gate");
|