peeksafe 0.0.0-stage → 0.1.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +78 -0
  4. package/README.md +492 -2
  5. package/RESEARCH-NOTES.md +274 -0
  6. package/dist/baseline.d.ts +45 -0
  7. package/dist/baseline.d.ts.map +1 -0
  8. package/dist/baseline.js +49 -0
  9. package/dist/baseline.js.map +1 -0
  10. package/dist/check.d.ts +64 -0
  11. package/dist/check.d.ts.map +1 -0
  12. package/dist/check.js +172 -0
  13. package/dist/check.js.map +1 -0
  14. package/dist/cluster.d.ts +328 -0
  15. package/dist/cluster.d.ts.map +1 -0
  16. package/dist/cluster.js +598 -0
  17. package/dist/cluster.js.map +1 -0
  18. package/dist/errors.d.ts +87 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +143 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/evidence.d.ts +15 -0
  23. package/dist/evidence.d.ts.map +1 -0
  24. package/dist/evidence.js +30 -0
  25. package/dist/evidence.js.map +1 -0
  26. package/dist/frontier.d.ts +269 -0
  27. package/dist/frontier.d.ts.map +1 -0
  28. package/dist/frontier.js +460 -0
  29. package/dist/frontier.js.map +1 -0
  30. package/dist/gate.d.ts +173 -0
  31. package/dist/gate.d.ts.map +1 -0
  32. package/dist/gate.js +196 -0
  33. package/dist/gate.js.map +1 -0
  34. package/dist/index.d.ts +60 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +79 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/plan.d.ts +248 -0
  39. package/dist/plan.d.ts.map +1 -0
  40. package/dist/plan.js +482 -0
  41. package/dist/plan.js.map +1 -0
  42. package/dist/rand.d.ts +17 -0
  43. package/dist/rand.d.ts.map +1 -0
  44. package/dist/rand.js +92 -0
  45. package/dist/rand.js.map +1 -0
  46. package/dist/stats.d.ts +397 -0
  47. package/dist/stats.d.ts.map +1 -0
  48. package/dist/stats.js +1245 -0
  49. package/dist/stats.js.map +1 -0
  50. package/dist/stop.d.ts +163 -0
  51. package/dist/stop.d.ts.map +1 -0
  52. package/dist/stop.js +262 -0
  53. package/dist/stop.js.map +1 -0
  54. package/package.json +64 -4
  55. package/src/baseline.ts +72 -0
  56. package/src/check.ts +238 -0
  57. package/src/cluster.ts +830 -0
  58. package/src/errors.ts +208 -0
  59. package/src/evidence.ts +62 -0
  60. package/src/frontier.ts +782 -0
  61. package/src/gate.ts +358 -0
  62. package/src/index.ts +206 -0
  63. package/src/plan.ts +778 -0
  64. package/src/rand.ts +121 -0
  65. package/src/stats.ts +1579 -0
  66. package/src/stop.ts +452 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * frontier.ts: what *is* certifiable at coding-agent prices, and how.
3
+ *
4
+ * ## Why this module exists
5
+ *
6
+ * At $1 a run a 200-case suite at a 15-point MDE is $1,320 a pull request, so
7
+ * peeksafe is not a per-PR gate for a coding-agent benchmark at that size. That
8
+ * is true and it is half an answer. The other half, the one a
9
+ * buyer actually needs, is **which configurations *are* affordable**, and
10
+ * nobody had drawn it.
11
+ *
12
+ * A configuration is a point in five dimensions:
13
+ *
14
+ * suite size m × MDE δ × design × screening × baseline runs n_b
15
+ *
16
+ * and it is usable only if it is *both* affordable and **certifiable**. Those
17
+ * are different constraints, and a plan that conflates them quotes the wrong
18
+ * numbers. Cheap is easy: run fewer cases. Cheap *and able to decide anything* is
19
+ * the hard part, because `evidenceCeilingLogE` says a case whose unpaired
20
+ * ceiling sits under the e-BH bar cannot be certified at any budget, and the
21
+ * bar `m/q` moves with the suite size, so shrinking the suite raises the
22
+ * ceiling's odds *and* lowers the bar at the same time. That interaction is not
23
+ * something you can eyeball.
24
+ *
25
+ * ## The cost model, and the term the plan was missing
26
+ *
27
+ * `plan.ts` prices an observation at one number, `costPerRunUsd`. That is wrong
28
+ * in two ways at coding-agent prices, and both matter:
29
+ *
30
+ * 1. **Grading is not free.** An LLM judge is a model call; a coding-agent
31
+ * grader is a test suite in a container. So
32
+ *
33
+ * cost(observation) = costPerRunUsd + costPerGradeUsd
34
+ *
35
+ * and the second term is the *only* one a cheap proxy grader can reduce.
36
+ * When `costPerGradeUsd` is 0, which is the default,
37
+ * matching this package's own free graders, a proxy screen saves exactly
38
+ * nothing, and this module says so instead of offering it as an economy.
39
+ *
40
+ * 2. **The baseline is not free either, and it is not amortised over
41
+ * infinity.** The README calls the baseline "amortised, you pay it once on
42
+ * main". At $0.002 a run that is a rounding error. At $1 a run, a 200-case
43
+ * suite with a 240-run baseline is 48,000 runs, **$48,000**, and spread
44
+ * over a hundred pull requests it is still $480 each, which is larger than
45
+ * everything else in the bill put together. The unpaired design's evidence
46
+ * ceiling is bought with baseline runs, so *the amortised baseline is the
47
+ * dominant cost of the unpaired design at coding-agent prices*, and the
48
+ * paired design's headline advantage is not variance reduction. It is that
49
+ * it does not need to buy a ceiling.
50
+ *
51
+ * ## What this module will not do
52
+ *
53
+ * It will not recommend a screened configuration whose **recall cost has not
54
+ * been measured**. Screening is the largest cost lever available and it is paid
55
+ * for in missed regressions, which the caller supplies as `screenRecall`
56
+ * against planted ground truth, and until someone has, the honest price of a
57
+ * screened plan is unknown rather than low.
58
+ */
59
+ import { PeeksafeError, requireErrorRates, requireOpenProbability, requireProbability, } from "./errors.js";
60
+ import { DEFAULT_PLAN } from "./plan.js";
61
+ import { betaQuantile, ebhSoloThreshold, evidenceCeilingLogE, mcnemarSamplesForEvidence, samplesForEvidence, sprtExpectedN, } from "./stats.js";
62
+ export const DEFAULT_AXES = {
63
+ cases: [10, 20, 50, 100, 200],
64
+ mdes: [0.1, 0.15, 0.25, 0.35],
65
+ baselineRuns: [30, 60, 120, 240, 480],
66
+ designs: ["unpaired", "paired"],
67
+ screens: ["none", "expensive", "proxy"],
68
+ screenRuns: [4, 8],
69
+ };
70
+ export const DEFAULT_FRONTIER = {
71
+ budgetUsd: 100,
72
+ basis: "typical",
73
+ costPerRunUsd: 1,
74
+ costPerGradeUsd: 0,
75
+ baselineRate: 0.85,
76
+ fdr: 0.05,
77
+ pairCoupling: DEFAULT_PLAN.pairCoupling,
78
+ amortisePrs: 100,
79
+ pairedBaselineRuns: 20,
80
+ screenKeepFraction: 0.15,
81
+ runsPerCaseCeiling: DEFAULT_PLAN.runsPerCaseCeiling,
82
+ screenRecall: { expensive: null, proxy: null },
83
+ screenRecallSource: "not measured",
84
+ alpha: DEFAULT_PLAN.alpha,
85
+ beta: DEFAULT_PLAN.beta,
86
+ minTrials: DEFAULT_PLAN.minTrials,
87
+ maxTrials: DEFAULT_PLAN.maxTrials,
88
+ batchSize: DEFAULT_PLAN.batchSize,
89
+ nullQuantile: DEFAULT_PLAN.nullQuantile,
90
+ };
91
+ const RUNS_PER_OBS = { unpaired: 1, paired: 2 };
92
+ /** $1.00, but $0.0021 rather than $0.00, a per-run price rounds to nothing. */
93
+ export const fmtRate = (x) => !Number.isFinite(x)
94
+ ? Number.isNaN(x)
95
+ ? "$?"
96
+ : "$∞"
97
+ : x >= 0.01 || x === 0
98
+ ? `$${x.toFixed(2)}`
99
+ : `$${x.toPrecision(2)}`;
100
+ /** Cost of `runs` runs, all of them graded by the real grader. */
101
+ const graded = (runs, cfg) => runs * (cfg.costPerRunUsd + cfg.costPerGradeUsd);
102
+ /**
103
+ * Observations per case on a pull request where nothing moved, clamped onto the
104
+ * gate's own batch grid.
105
+ */
106
+ export function typicalObservationsPerCase(rate, mde, cfg, baselineRuns) {
107
+ // `makePlan` reaches this quantile through `baselineNullRate`, which refuses
108
+ // one outside (0,1). This handed it to `betaQuantile` directly, which answers
109
+ // 0 for a quantile at or below 0, 1 at or above 1, and about 0 for NaN, so the
110
+ // frontier priced the typical bill against a null rate nobody chose.
111
+ requireOpenProbability(cfg.nullQuantile, "nullQuantile", "typicalObservationsPerCase");
112
+ const s = Math.round(rate * baselineRuns);
113
+ const p0 = betaQuantile(1 + s, 1 + baselineRuns - s, cfg.nullQuantile);
114
+ const p1 = Math.max(0.005, p0 - mde);
115
+ const raw = sprtExpectedN(rate, p0, p1, cfg.alpha, cfg.beta);
116
+ const n = Number.isFinite(raw) && raw > 0
117
+ ? Math.min(cfg.maxTrials, Math.max(cfg.minTrials, Math.ceil(raw)))
118
+ : cfg.maxTrials;
119
+ const batched = cfg.minTrials + Math.ceil(Math.max(0, n - cfg.minTrials) / cfg.batchSize) * cfg.batchSize;
120
+ return Math.min(cfg.maxTrials, batched);
121
+ }
122
+ export function evaluatePoint(cases, mde, baselineRuns, design, screen, screenRunsPerCase, cfg) {
123
+ // Strictly open: an MDE of 0 means "detect a zero-point drop", which every
124
+ // design correctly refuses, but it refuses with a message about the evidence
125
+ // ceiling, which sends the reader looking for a baseline problem that is not
126
+ // there. Same for a baseline rate of 0 or 1.
127
+ requireOpenProbability(mde, "mde", "evaluatePoint");
128
+ requireOpenProbability(cfg.baselineRate, "baselineRate", "evaluatePoint");
129
+ // `makePlan` refuses these two and this did not, so one options object was a
130
+ // refusal through the planner and a priced frontier through here. Both end up
131
+ // in `sprtExpectedN`, which answers NaN or Infinity outside (0,1), and
132
+ // `typicalObservationsPerCase` reads either as "cannot price this" and falls
133
+ // back to the run ceiling.
134
+ requireErrorRates(cfg.alpha, cfg.beta, "evaluatePoint");
135
+ requireProbability(cfg.pairCoupling, "pairCoupling", "evaluatePoint");
136
+ requireOpenProbability(cfg.nullQuantile, "nullQuantile", "evaluatePoint");
137
+ if (!Number.isInteger(cases) || cases < 1) {
138
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `frontier: cases must be a positive integer, got ${cases}`, {
139
+ detail: { cases },
140
+ });
141
+ }
142
+ if (!Number.isInteger(baselineRuns) || baselineRuns < 1) {
143
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `frontier: baselineRuns must be a positive integer, got ${baselineRuns}`, {
144
+ detail: { baselineRuns },
145
+ });
146
+ }
147
+ if (!Number.isInteger(cfg.maxTrials) || cfg.maxTrials < 1 || cfg.maxTrials < cfg.minTrials) {
148
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `frontier: maxTrials must be a positive integer ≥ minTrials, got ${cfg.maxTrials} vs ${cfg.minTrials}`, {
149
+ detail: { maxTrials: cfg.maxTrials, minTrials: cfg.minTrials },
150
+ hint: "the ceiling bill is priced at this cap; a cap of zero prices a gate that never runs",
151
+ });
152
+ }
153
+ if (screen !== "none" && (!Number.isInteger(screenRunsPerCase) || screenRunsPerCase < 1)) {
154
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `frontier: a ${screen} screen needs at least one run per case, got ${screenRunsPerCase}`, {
155
+ detail: { screen, screenRunsPerCase },
156
+ });
157
+ }
158
+ const rate = cfg.baselineRate;
159
+ const barLogE = Math.log(ebhSoloThreshold(cases, cfg.fdr));
160
+ // The baseline this design actually needs. The paired design's null is exact,
161
+ // so its stored baseline only tunes the stopping rule: a short one suffices,
162
+ // and pretending it needs the same n_b as the unpaired design would price a
163
+ // ceiling it never buys.
164
+ const effectiveBaselineRuns = design === "paired" ? cfg.pairedBaselineRuns : baselineRuns;
165
+ const s = Math.round(rate * effectiveBaselineRuns);
166
+ let ceilingLogE = null;
167
+ let certifyObservations;
168
+ let infeasibleBecause = null;
169
+ if (rate <= mde) {
170
+ infeasibleBecause =
171
+ `a case at ${(rate * 100).toFixed(0)}% cannot lose ${(mde * 100).toFixed(0)} points, ` +
172
+ "the effect is arithmetically impossible, not merely expensive";
173
+ certifyObservations = Infinity;
174
+ }
175
+ else if (design === "unpaired") {
176
+ const pTrue = Math.max(1e-6, Math.min(1 - 1e-6, rate - mde));
177
+ ceilingLogE = evidenceCeilingLogE(pTrue, s, effectiveBaselineRuns, mde);
178
+ certifyObservations = samplesForEvidence(pTrue, s, effectiveBaselineRuns, mde, barLogE, cfg.runsPerCaseCeiling * 8);
179
+ if (!Number.isFinite(certifyObservations)) {
180
+ infeasibleBecause =
181
+ `unpaired evidence ceiling e^${ceilingLogE.toFixed(1)} is below the bar e^${barLogE.toFixed(1)} ` +
182
+ `at a ${effectiveBaselineRuns}-run baseline, no candidate budget certifies it`;
183
+ }
184
+ }
185
+ else {
186
+ const pairs = mcnemarSamplesForEvidence(rate, mde, cfg.pairCoupling, barLogE, cfg.runsPerCaseCeiling * 8);
187
+ certifyObservations = pairs;
188
+ if (!Number.isFinite(pairs)) {
189
+ infeasibleBecause = `paired: no number of pairs reaches the bar at ρ = ${cfg.pairCoupling.toFixed(2)}`;
190
+ }
191
+ }
192
+ const runsPerObs = RUNS_PER_OBS[design];
193
+ if (infeasibleBecause === null && certifyObservations * runsPerObs > cfg.runsPerCaseCeiling) {
194
+ infeasibleBecause =
195
+ `${Math.ceil(certifyObservations * runsPerObs)} runs for one case, past the ` +
196
+ `${cfg.runsPerCaseCeiling}-run per-case ceiling`;
197
+ }
198
+ const feasible = infeasibleBecause === null;
199
+ const typicalObservations = typicalObservationsPerCase(rate, mde, cfg, effectiveBaselineRuns);
200
+ const keep = screen === "none" ? cases : Math.max(1, Math.ceil(cases * cfg.screenKeepFraction));
201
+ // Screening runs are graded by whichever grader the screen uses. A proxy
202
+ // screen still *executes* the agent, it only skips the grade, so it saves
203
+ // `costPerGradeUsd` per screening run and not one cent more.
204
+ //
205
+ // And a screening *observation* costs `runsPerObs` runs, exactly as a test
206
+ // observation does, since a paired screen runs both arms too. Charging it one
207
+ // run an observation would understate it by 2×, in the direction that makes
208
+ // pairing look cheaper.
209
+ const screenRuns = screen === "none" ? 0 : cases * screenRunsPerCase * runsPerObs;
210
+ const screenUsd = screen === "none"
211
+ ? 0
212
+ : screen === "proxy"
213
+ ? screenRuns * cfg.costPerRunUsd
214
+ : graded(screenRuns, cfg);
215
+ // The baseline. Unpaired buys its ceiling here; paired does not.
216
+ const baselineRunsTotal = cases * effectiveBaselineRuns;
217
+ const baselineRunsAmortised = baselineRunsTotal / Math.max(1, cfg.amortisePrs);
218
+ const baselineUsd = graded(baselineRunsAmortised, cfg);
219
+ const costFor = (obsPerCase) => {
220
+ const testRuns = Number.isFinite(obsPerCase) ? keep * obsPerCase * runsPerObs : Infinity;
221
+ const testUsd = Number.isFinite(testRuns) ? graded(testRuns, cfg) : Infinity;
222
+ return {
223
+ screenRuns,
224
+ screenUsd,
225
+ testRuns,
226
+ testUsd,
227
+ baselineRunsAmortised,
228
+ baselineUsd,
229
+ totalUsd: screenUsd + testUsd + baselineUsd,
230
+ };
231
+ };
232
+ const typical = costFor(typicalObservations);
233
+ const certifyAll = costFor(certifyObservations);
234
+ // The per-case cap this configuration would have to run under.
235
+ //
236
+ // `samplesForEvidence` can answer more runs than `cfg.maxTrials`, and a plan
237
+ // quoting those prices a design the configured cap never executes. So the cap
238
+ // is whatever this configuration *needs*, and `requiresMaxRuns` says so.
239
+ const capObservations = Number.isFinite(certifyObservations)
240
+ ? Math.max(cfg.maxTrials, Math.ceil(certifyObservations))
241
+ : cfg.maxTrials;
242
+ // The gate stops paying for a case at that cap, so this is a real upper
243
+ // bound rather than a scenario, and scenarios do not bound anything.
244
+ const ceiling = costFor(capObservations);
245
+ const screenRecall = screen === "none"
246
+ ? 1
247
+ : screen === "proxy"
248
+ ? cfg.screenRecall.proxy
249
+ : cfg.screenRecall.expensive;
250
+ // A lookup, not a `?:` chain with `typical` on the else branch. A typo'd
251
+ // basis silently selecting the cheapest bill is precisely the
252
+ // malformed-input-becomes-the-most-permissive-result failure this codebase
253
+ // keeps finding in itself.
254
+ const BILLS = {
255
+ typical,
256
+ "certify-all": certifyAll,
257
+ ceiling,
258
+ };
259
+ const basisCost = BILLS[cfg.basis];
260
+ if (basisCost === undefined) {
261
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `frontier: basis must be one of ${Object.keys(BILLS).join(", ")}, got "${String(cfg.basis)}"`, {
262
+ detail: { basis: cfg.basis },
263
+ hint: "an unrecognised basis silently defaulting to the cheaper bill is how a budget becomes fiction",
264
+ });
265
+ }
266
+ const withinBudget = feasible && basisCost.totalUsd <= cfg.budgetUsd;
267
+ // Reportable and recommendable are different. A screen whose recall nobody
268
+ // measured is cheap in a way we cannot price, and a proxy screen at zero
269
+ // grading cost is cheaper than nothing at all, which is to say, it is not.
270
+ let caveat = null;
271
+ if (screen !== "none" && screenRecall === null) {
272
+ caveat = `the ${screen} screen's recall cost is unmeasured, it drops regressions at an unknown rate`;
273
+ }
274
+ else if (screen === "proxy" && cfg.costPerGradeUsd <= 0) {
275
+ caveat =
276
+ "a proxy screen saves only grading cost, and grading here is free, it buys nothing and costs recall";
277
+ }
278
+ return {
279
+ cases,
280
+ mde,
281
+ baselineRuns,
282
+ design,
283
+ screen,
284
+ screenRunsPerCase: screen === "none" ? 0 : screenRunsPerCase,
285
+ keep,
286
+ barLogE,
287
+ ceilingLogE,
288
+ feasible,
289
+ infeasibleBecause,
290
+ certifyObservations,
291
+ typicalObservations,
292
+ requiresMaxRuns: capObservations,
293
+ exceedsGateCap: capObservations > cfg.maxTrials,
294
+ typical,
295
+ certifyAll,
296
+ ceiling,
297
+ basisCostUsd: basisCost.totalUsd,
298
+ withinBudget,
299
+ screenRecall,
300
+ recommendable: withinBudget && caveat === null,
301
+ caveat,
302
+ };
303
+ }
304
+ /** Every point in the swept space, feasible or not. */
305
+ export function enumerateFrontier(cfg, axes = DEFAULT_AXES) {
306
+ if (!(cfg.budgetUsd >= 0) || !(cfg.costPerRunUsd >= 0) || !(cfg.costPerGradeUsd >= 0)) {
307
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", "frontier: budget and costs must be ≥ 0 and not NaN", {
308
+ detail: {
309
+ budgetUsd: cfg.budgetUsd,
310
+ costPerRunUsd: cfg.costPerRunUsd,
311
+ costPerGradeUsd: cfg.costPerGradeUsd,
312
+ },
313
+ });
314
+ }
315
+ if (!(cfg.screenKeepFraction > 0) || !(cfg.screenKeepFraction <= 1)) {
316
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `frontier: screenKeepFraction must be in (0,1], got ${cfg.screenKeepFraction}`, {
317
+ detail: { screenKeepFraction: cfg.screenKeepFraction },
318
+ });
319
+ }
320
+ if (!Number.isInteger(cfg.amortisePrs) || cfg.amortisePrs < 1) {
321
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `frontier: amortisePrs must be a positive integer, got ${cfg.amortisePrs}`, {
322
+ detail: { amortisePrs: cfg.amortisePrs },
323
+ hint: 'a baseline spread over "infinity" pull requests is how the baseline cost disappears from a plan',
324
+ });
325
+ }
326
+ for (const [name, xs] of Object.entries(axes)) {
327
+ if (!Array.isArray(xs) || xs.length === 0) {
328
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `frontier: axis "${name}" is empty, nothing to sweep`, {
329
+ detail: { axis: name },
330
+ });
331
+ }
332
+ }
333
+ const out = [];
334
+ for (const m of axes.cases) {
335
+ for (const mde of axes.mdes) {
336
+ for (const design of axes.designs) {
337
+ // The baseline axis is real for the unpaired design and a fiction for
338
+ // the paired one, which uses `pairedBaselineRuns` whatever this says.
339
+ // Sweeping it anyway would emit five identical paired rows and make the
340
+ // frontier look like it had five times the evidence it has.
341
+ const baselines = design === "paired" ? [cfg.pairedBaselineRuns] : axes.baselineRuns;
342
+ for (const nb of baselines) {
343
+ for (const screen of axes.screens) {
344
+ const ks = screen === "none" ? [0] : axes.screenRuns;
345
+ for (const k of ks)
346
+ out.push(evaluatePoint(m, mde, nb, design, screen, k, cfg));
347
+ }
348
+ }
349
+ }
350
+ }
351
+ }
352
+ return out;
353
+ }
354
+ /**
355
+ * A cell (m, δ) dominates (m', δ') when it covers at least as many cases and
356
+ * detects at least as small a drop, and is strictly better on one of them. The
357
+ * frontier is what survives.
358
+ */
359
+ function paretoCells(cells) {
360
+ return cells.filter((a) => !cells.some((b) => b !== a && b.cases >= a.cases && b.mde <= a.mde && (b.cases > a.cases || b.mde < a.mde)));
361
+ }
362
+ export function computeFrontier(cfg, axes = DEFAULT_AXES) {
363
+ const points = enumerateFrontier(cfg, axes);
364
+ const feasible = points.filter((p) => p.feasible);
365
+ const affordable = points.filter((p) => p.recommendable);
366
+ const cheaper = (a, b) => a.basisCostUsd <= b.basisCostUsd ? a : b;
367
+ const byCell = new Map();
368
+ for (const p of affordable) {
369
+ const key = `${p.cases}|${p.mde}`;
370
+ const cur = byCell.get(key);
371
+ if (!cur)
372
+ byCell.set(key, { cases: p.cases, mde: p.mde, best: p, cheapestReported: null });
373
+ else
374
+ cur.best = cheaper(cur.best, p);
375
+ }
376
+ // For each cell on the frontier, also name the cheapest *reported* option, so
377
+ // an operator can see what the "unmeasured recall" rule cost them.
378
+ for (const cell of byCell.values()) {
379
+ const reported = points
380
+ .filter((p) => p.cases === cell.cases && p.mde === cell.mde && p.withinBudget)
381
+ .reduce((best, p) => (best === null ? p : cheaper(best, p)), null);
382
+ cell.cheapestReported =
383
+ reported && reported.basisCostUsd < cell.best.basisCostUsd ? reported : null;
384
+ }
385
+ const frontier = paretoCells([...byCell.values()]).sort((a, b) => b.cases - a.cases || a.mde - b.mde);
386
+ const cheapestFeasible = feasible.reduce((best, p) => (best === null || p.basisCostUsd < best.basisCostUsd ? p : best), null);
387
+ // The smallest budget at which *something* becomes recommendable. `caveat`
388
+ // is exactly the recommendable rule minus the budget test, so reusing it here
389
+ // means the headline and the recommendation cannot disagree about what counts.
390
+ const cleanFeasible = feasible.filter((p) => p.caveat === null);
391
+ const minimumViableBudgetUsd = cleanFeasible.length === 0 ? null : Math.min(...cleanFeasible.map((p) => p.basisCostUsd));
392
+ const headline = frontier.length === 0
393
+ ? minimumViableBudgetUsd === null
394
+ ? `nothing in the swept space is certifiable at ${(cfg.baselineRate * 100).toFixed(0)}% baseline rate, ` +
395
+ "every configuration is either arithmetically impossible or past the per-case run ceiling"
396
+ : `nothing fits $${cfg.budgetUsd.toFixed(2)} at ${fmtRate(cfg.costPerRunUsd)}/run on the ${cfg.basis} bill. ` +
397
+ `The cheapest certifiable configuration is $${minimumViableBudgetUsd.toFixed(2)} per pull request` +
398
+ (cheapestFeasible
399
+ ? `, ${cheapestFeasible.cases} cases at ${(cheapestFeasible.mde * 100).toFixed(0)} points, ${cheapestFeasible.design}.`
400
+ : ".")
401
+ : (() => {
402
+ const widest = frontier[0];
403
+ const tightest = [...frontier].sort((a, b) => a.mde - b.mde)[0];
404
+ return (`at ${fmtRate(cfg.costPerRunUsd)}/run and $${cfg.budgetUsd.toFixed(2)} a pull request (${cfg.basis} bill) you can certify ` +
405
+ `${widest.cases} cases at ${(widest.mde * 100).toFixed(0)} points ` +
406
+ `($${widest.best.basisCostUsd.toFixed(2)}, ${widest.best.design})` +
407
+ (tightest !== widest
408
+ ? `, or ${tightest.cases} cases at ${(tightest.mde * 100).toFixed(0)} points ` +
409
+ `($${tightest.best.basisCostUsd.toFixed(2)}, ${tightest.best.design})`
410
+ : "") +
411
+ ".");
412
+ })();
413
+ return {
414
+ config: cfg,
415
+ axes,
416
+ points,
417
+ affordable,
418
+ frontier,
419
+ cheapestFeasible,
420
+ minimumViableBudgetUsd,
421
+ headline,
422
+ };
423
+ }
424
+ /**
425
+ * Where the money goes, for one configuration. Printed under the frontier
426
+ * because the shares are the actual finding: at coding-agent prices the
427
+ * unpaired design's amortised baseline is usually the largest line, and it is
428
+ * the one the README used to call free.
429
+ */
430
+ export function costShares(p, basis = "typical") {
431
+ // A lookup and a refusal, for the same reason `evaluatePoint` uses one: this
432
+ // was a `?:` chain with `p.typical` on the else branch, so any basis it did
433
+ // not recognise was reported as the typical bill. `FrontierPoint` names the
434
+ // field `certifyAll` and the basis `certify-all`, so reaching for the field
435
+ // name is the obvious slip, and it put the baseline at 6.6% of the money
436
+ // where the certify-all bill puts it at 1.1%.
437
+ const BILLS = {
438
+ typical: p.typical,
439
+ "certify-all": p.certifyAll,
440
+ ceiling: p.ceiling,
441
+ };
442
+ // `hasOwn` rather than an `undefined` check: `BILLS['toString']` is inherited
443
+ // from Object.prototype, so it is not undefined and would slip through one.
444
+ if (!Object.hasOwn(BILLS, basis)) {
445
+ throw new PeeksafeError("PEEKSAFE_E_CONFIG", `costShares: basis must be one of ${Object.keys(BILLS).join(", ")}, got "${String(basis)}"`, {
446
+ detail: { basis },
447
+ hint: "an unrecognised basis silently reporting the typical bill is how a cost breakdown describes a different plan than the one you asked about",
448
+ });
449
+ }
450
+ const cost = BILLS[basis];
451
+ const t = cost.totalUsd;
452
+ if (!(t > 0) || !Number.isFinite(t))
453
+ return { screen: 0, test: 0, baseline: 0 };
454
+ return {
455
+ screen: cost.screenUsd / t,
456
+ test: cost.testUsd / t,
457
+ baseline: cost.baselineUsd / t,
458
+ };
459
+ }
460
+ //# sourceMappingURL=frontier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontier.js","sourceRoot":"","sources":["../src/frontier.ts"],"names":[],"mappings":"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"}
package/dist/gate.d.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * gate.ts: the decision.
3
+ *
4
+ * You bring counts. This decides which cases regressed, at a false discovery
5
+ * rate you name, in a way that stays valid however many times you looked at the
6
+ * data on the way here.
7
+ *
8
+ * ## Why this is safe to peek at
9
+ *
10
+ * The usual construction is: run the suite, stop a case when it looks
11
+ * significant, compute a p-value from the counts, hand the p-values to
12
+ * Benjamini-Hochberg. That is invalid in two independent places.
13
+ *
14
+ * A p-value computed at a stopping boundary is not a p-value. `P(p_n <= a) <= a`
15
+ * holds for each fixed `n`; a procedure that adds runs and re-tests until the
16
+ * result looks significant reports `min` over the peeks, and that minimum is not
17
+ * bounded by `a`. Benjamini-Hochberg takes `P(p <= t) <= t` as its input
18
+ * assumption, so it inherits nothing and controls nothing.
19
+ *
20
+ * This module never computes a p-value. It computes an e-value, which is a
21
+ * non-negative statistic with mean at most 1 under the null. Ville's inequality
22
+ * bounds `P(sup_n E_n >= 1/a) <= a` over the whole trajectory, so an e-value is
23
+ * valid at any stopping time, including one chosen after looking. (The mean is
24
+ * exactly 1 averaged over the shared rate, not at every fixed rate; the README
25
+ * section "What that guarantee rests on" says where the bound is checked.) The
26
+ * multiplicity correction is e-BH (Wang and Ramdas), which takes e-values
27
+ * directly and controls FDR under arbitrary dependence between cases.
28
+ *
29
+ * The practical consequence: you may stop a case whenever you like, for any
30
+ * reason, including "it already looks bad". Nothing here cares when you stopped.
31
+ *
32
+ * ## What it will refuse to do
33
+ *
34
+ * Gate a case with no baseline. There is no null to test against, and the
35
+ * failure mode of inventing one is not a wrong number, it is a confident wrong
36
+ * number: a missing baseline read as `0/0` has a Beta(1,1) posterior median of
37
+ * 0.5, which gates the case against "it passes half the time" and reports a
38
+ * large improvement against nothing at all. Cases without a baseline come back
39
+ * in `newCases` and take no part in the verdict or the e-BH family.
40
+ *
41
+ * ## Paired cases
42
+ *
43
+ * A case may instead carry `paired` counts from re-running the baseline next to
44
+ * the candidate, one baseline run per candidate run. Its e-value is
45
+ * `pairedLogE` on the discordant pairs, which is valid at every rate by
46
+ * construction and has no ceiling, whichever `evidence` the unpaired cases use.
47
+ * The pairs need not share a seed: independent runs paired in order are still
48
+ * valid, and a shared seed only makes discordances more informative.
49
+ */
50
+ import type { BaselineStat } from "./baseline.js";
51
+ import { type Evidence } from "./evidence.js";
52
+ import { type PairedCounts } from "./stats.js";
53
+ /**
54
+ * True when the baseline's observed rate leaves no room for an `mde`-sized drop.
55
+ * `makePlan` calls the same cases IMPOSSIBLE, by the same rule.
56
+ */
57
+ export declare const cannotDropBy: (baseline: {
58
+ successes: number;
59
+ trials: number;
60
+ }, mde: number) => boolean;
61
+ /** One case's candidate result, and the baseline it is measured against. */
62
+ export interface GateCase {
63
+ id: string;
64
+ /** Successes observed for the revision under test. */
65
+ successes: number;
66
+ /** Trials run for the revision under test. Stop whenever you like. */
67
+ trials: number;
68
+ /** The reference revision's recorded counts, or undefined if there are none. */
69
+ baseline?: BaselineStat;
70
+ paired?: undefined;
71
+ }
72
+ /** One case run as pairs: each candidate run next to a fresh baseline run. */
73
+ export interface PairedGateCase {
74
+ id: string;
75
+ /** The pair table so far. Stop whenever you like. */
76
+ paired: PairedCounts;
77
+ }
78
+ export interface GateOptions {
79
+ /** Minimum detectable effect: the drop, in rate, that counts as a regression. */
80
+ mde?: number;
81
+ /** Target false discovery rate across the suite. */
82
+ fdr?: number;
83
+ /**
84
+ * Concentration of the alternative prior. Lower spreads the alternative over
85
+ * "anything materially worse", which is closer to how a broken case behaves
86
+ * than a point mass exactly `mde` below the baseline. At most
87
+ * `MAX_ALT_CONCENTRATION` (16) with the default `bayes` statistic.
88
+ */
89
+ altConcentration?: number;
90
+ /**
91
+ * Which e-value decides. `bayes` has more power and error control checked
92
+ * numerically; `universal` is valid at every rate by construction and needs
93
+ * more runs. See the README section "What that guarantee rests on".
94
+ */
95
+ evidence?: Evidence;
96
+ }
97
+ export declare const DEFAULT_GATE_OPTIONS: {
98
+ readonly mde: 0.15;
99
+ readonly fdr: 0.05;
100
+ readonly altConcentration: 8;
101
+ readonly evidence: "bayes";
102
+ };
103
+ export interface CaseVerdict {
104
+ id: string;
105
+ /** True when e-BH certifies this case as regressed at the suite's FDR. */
106
+ regressed: boolean;
107
+ /** The e-value. Larger is more evidence against the null. */
108
+ evalue: number;
109
+ /** log of the above, which is what the arithmetic actually uses. */
110
+ logE: number;
111
+ /**
112
+ * The largest e-value this case could ever reach against its current
113
+ * baseline, however many candidate runs you add. When this is below
114
+ * `threshold`, the case is undetectable at any candidate budget and the only
115
+ * remedy is more baseline runs.
116
+ */
117
+ ceiling: number;
118
+ /** True when `ceiling` is below the bar the case has to clear. */
119
+ undetectable: boolean;
120
+ /**
121
+ * True when the baseline rate is at or below `mde`, so an `mde`-sized drop
122
+ * cannot happen. The case is not tested: its e-value is fixed at 1, which
123
+ * keeps it in the e-BH family without it ever being certified.
124
+ */
125
+ impossible: boolean;
126
+ /** For a paired case, each arm's passes over the number of pairs. */
127
+ observed: {
128
+ successes: number;
129
+ trials: number;
130
+ };
131
+ baseline: {
132
+ successes: number;
133
+ trials: number;
134
+ };
135
+ design: "unpaired" | "paired";
136
+ /** The pair table, for a paired case. */
137
+ paired?: PairedCounts;
138
+ }
139
+ export interface GateResult {
140
+ /** PASS when nothing was certified as regressed. */
141
+ verdict: "PASS" | "FAIL";
142
+ cases: CaseVerdict[];
143
+ /** Cases certified as regressed, in the order given. */
144
+ regressed: CaseVerdict[];
145
+ /**
146
+ * Cases with no baseline. These are reported, not gated: they have no null to
147
+ * be tested against and they are excluded from the e-BH family.
148
+ */
149
+ newCases: string[];
150
+ /** The e-value a case had to beat, given how many discoveries were made. */
151
+ threshold: number;
152
+ /** The e-value a case must reach to be certified whatever the others do. */
153
+ soloThreshold: number;
154
+ options: Required<GateOptions>;
155
+ /**
156
+ * One line an operator can read without knowing what an e-value is.
157
+ *
158
+ * Names the two things a passing suite can hide: cases with no baseline,
159
+ * which were not gated at all, and cases whose baseline is too thin to
160
+ * certify an `mde`-sized drop however many runs you add. A PASS that is
161
+ * really "nothing was checked" should not read like a PASS.
162
+ */
163
+ headline: string;
164
+ }
165
+ /**
166
+ * Decide which cases regressed.
167
+ *
168
+ * Valid at any stopping time. Run every case to a fixed count, or stop each one
169
+ * the moment it looks decided, or anything in between; the guarantee does not
170
+ * depend on the rule you used.
171
+ */
172
+ export declare function gate(cases: readonly (GateCase | PairedGateCase)[], options?: GateOptions): GateResult;
173
+ //# sourceMappingURL=gate.d.ts.map