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
package/src/gate.ts ADDED
@@ -0,0 +1,358 @@
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
+
51
+ import type { BaselineStat } from "./baseline.js";
52
+ import {
53
+ PeeksafeError,
54
+ requireAltConcentration,
55
+ requireCounts,
56
+ requireOpenProbability,
57
+ requirePairedCounts,
58
+ } from "./errors.js";
59
+ import {
60
+ ceilingLogEvidence,
61
+ type Evidence,
62
+ logEvidence,
63
+ requireEvidence,
64
+ toEvalue,
65
+ } from "./evidence.js";
66
+ import {
67
+ discordant,
68
+ ebhCorrect,
69
+ ebhSoloThreshold,
70
+ type PairedCounts,
71
+ pairedLogE,
72
+ } from "./stats.js";
73
+
74
+ /**
75
+ * True when the baseline's observed rate leaves no room for an `mde`-sized drop.
76
+ * `makePlan` calls the same cases IMPOSSIBLE, by the same rule.
77
+ */
78
+ export const cannotDropBy = (
79
+ baseline: { successes: number; trials: number },
80
+ mde: number,
81
+ ): boolean => baseline.successes / baseline.trials <= mde;
82
+
83
+ /** One case's candidate result, and the baseline it is measured against. */
84
+ export interface GateCase {
85
+ id: string;
86
+ /** Successes observed for the revision under test. */
87
+ successes: number;
88
+ /** Trials run for the revision under test. Stop whenever you like. */
89
+ trials: number;
90
+ /** The reference revision's recorded counts, or undefined if there are none. */
91
+ baseline?: BaselineStat;
92
+ paired?: undefined;
93
+ }
94
+
95
+ /** One case run as pairs: each candidate run next to a fresh baseline run. */
96
+ export interface PairedGateCase {
97
+ id: string;
98
+ /** The pair table so far. Stop whenever you like. */
99
+ paired: PairedCounts;
100
+ }
101
+
102
+ export interface GateOptions {
103
+ /** Minimum detectable effect: the drop, in rate, that counts as a regression. */
104
+ mde?: number;
105
+ /** Target false discovery rate across the suite. */
106
+ fdr?: number;
107
+ /**
108
+ * Concentration of the alternative prior. Lower spreads the alternative over
109
+ * "anything materially worse", which is closer to how a broken case behaves
110
+ * than a point mass exactly `mde` below the baseline. At most
111
+ * `MAX_ALT_CONCENTRATION` (16) with the default `bayes` statistic.
112
+ */
113
+ altConcentration?: number;
114
+ /**
115
+ * Which e-value decides. `bayes` has more power and error control checked
116
+ * numerically; `universal` is valid at every rate by construction and needs
117
+ * more runs. See the README section "What that guarantee rests on".
118
+ */
119
+ evidence?: Evidence;
120
+ }
121
+
122
+ export const DEFAULT_GATE_OPTIONS = {
123
+ mde: 0.15,
124
+ fdr: 0.05,
125
+ altConcentration: 8,
126
+ evidence: "bayes",
127
+ } as const;
128
+
129
+ export interface CaseVerdict {
130
+ id: string;
131
+ /** True when e-BH certifies this case as regressed at the suite's FDR. */
132
+ regressed: boolean;
133
+ /** The e-value. Larger is more evidence against the null. */
134
+ evalue: number;
135
+ /** log of the above, which is what the arithmetic actually uses. */
136
+ logE: number;
137
+ /**
138
+ * The largest e-value this case could ever reach against its current
139
+ * baseline, however many candidate runs you add. When this is below
140
+ * `threshold`, the case is undetectable at any candidate budget and the only
141
+ * remedy is more baseline runs.
142
+ */
143
+ ceiling: number;
144
+ /** True when `ceiling` is below the bar the case has to clear. */
145
+ undetectable: boolean;
146
+ /**
147
+ * True when the baseline rate is at or below `mde`, so an `mde`-sized drop
148
+ * cannot happen. The case is not tested: its e-value is fixed at 1, which
149
+ * keeps it in the e-BH family without it ever being certified.
150
+ */
151
+ impossible: boolean;
152
+ /** For a paired case, each arm's passes over the number of pairs. */
153
+ observed: { successes: number; trials: number };
154
+ baseline: { successes: number; trials: number };
155
+ design: "unpaired" | "paired";
156
+ /** The pair table, for a paired case. */
157
+ paired?: PairedCounts;
158
+ }
159
+
160
+ export interface GateResult {
161
+ /** PASS when nothing was certified as regressed. */
162
+ verdict: "PASS" | "FAIL";
163
+ cases: CaseVerdict[];
164
+ /** Cases certified as regressed, in the order given. */
165
+ regressed: CaseVerdict[];
166
+ /**
167
+ * Cases with no baseline. These are reported, not gated: they have no null to
168
+ * be tested against and they are excluded from the e-BH family.
169
+ */
170
+ newCases: string[];
171
+ /** The e-value a case had to beat, given how many discoveries were made. */
172
+ threshold: number;
173
+ /** The e-value a case must reach to be certified whatever the others do. */
174
+ soloThreshold: number;
175
+ options: Required<GateOptions>;
176
+ /**
177
+ * One line an operator can read without knowing what an e-value is.
178
+ *
179
+ * Names the two things a passing suite can hide: cases with no baseline,
180
+ * which were not gated at all, and cases whose baseline is too thin to
181
+ * certify an `mde`-sized drop however many runs you add. A PASS that is
182
+ * really "nothing was checked" should not read like a PASS.
183
+ */
184
+ headline: string;
185
+ }
186
+
187
+ /**
188
+ * Decide which cases regressed.
189
+ *
190
+ * Valid at any stopping time. Run every case to a fixed count, or stop each one
191
+ * the moment it looks decided, or anything in between; the guarantee does not
192
+ * depend on the rule you used.
193
+ */
194
+ export function gate(
195
+ cases: readonly (GateCase | PairedGateCase)[],
196
+ options: GateOptions = {},
197
+ ): GateResult {
198
+ const opts: Required<GateOptions> = { ...DEFAULT_GATE_OPTIONS, ...options };
199
+ requireOpenProbability(opts.mde, "mde", "gate");
200
+ requireOpenProbability(opts.fdr, "fdr", "gate");
201
+ requireEvidence(opts.evidence, "gate");
202
+ requireAltConcentration(opts.altConcentration, opts.evidence, "gate");
203
+
204
+ const seen = new Set<string>();
205
+ for (const c of cases) {
206
+ if (seen.has(c.id)) {
207
+ throw new PeeksafeError("PEEKSAFE_E_CASE_DUPLICATE", `gate: duplicate case id ${c.id}`, {
208
+ detail: { id: c.id },
209
+ hint: "two cases sharing an id would be merged into one baseline entry and counted once",
210
+ });
211
+ }
212
+ seen.add(c.id);
213
+ if (c.paired) {
214
+ requirePairedCounts(c.paired, `gate(${c.id}).paired`);
215
+ continue;
216
+ }
217
+ requireCounts(c.successes, c.trials, `gate(${c.id})`);
218
+ if (c.baseline)
219
+ requireCounts(c.baseline.successes, c.baseline.trials, `gate(${c.id}).baseline`);
220
+ }
221
+
222
+ const hasBaseline = (c: GateCase | PairedGateCase): boolean =>
223
+ c.paired !== undefined || (c.baseline !== undefined && c.baseline.trials > 0);
224
+ const newCases = cases.filter((c) => !hasBaseline(c)).map((c) => c.id);
225
+ const gated = cases.filter(hasBaseline);
226
+
227
+ if (gated.length === 0) {
228
+ throw new PeeksafeError(
229
+ "PEEKSAFE_E_BASELINE_MISSING",
230
+ "gate: no case has a baseline, so there is nothing to test against",
231
+ {
232
+ detail: { cases: cases.length, newCases: newCases.length },
233
+ hint: 'record a baseline on the reference revision first; gating without one compares against a Beta(1,1) prior, which reads as "this case passes half the time"',
234
+ },
235
+ );
236
+ }
237
+
238
+ const m = gated.length;
239
+ // Testing these anyway clamps the alternative onto a rate near zero, which is
240
+ // where the case already sits, and the false positive rate then exceeds `fdr`.
241
+ // A paired case has an exact null and never needs this.
242
+ const impossible = gated.map((c) => !c.paired && cannotDropBy(c.baseline!, opts.mde));
243
+ const logEs = gated.map((c, i) => {
244
+ if (c.paired) return pairedLogE(c.paired.worse, discordant(c.paired));
245
+ return impossible[i]
246
+ ? 0
247
+ : logEvidence(
248
+ opts.evidence,
249
+ c.successes,
250
+ c.trials,
251
+ c.baseline!.successes,
252
+ c.baseline!.trials,
253
+ opts.mde,
254
+ opts.altConcentration,
255
+ );
256
+ });
257
+ const evalues = logEs.map(toEvalue);
258
+ const ebh = ebhCorrect(evalues, opts.fdr);
259
+ const solo = ebhSoloThreshold(m, opts.fdr);
260
+
261
+ const verdicts: CaseVerdict[] = gated.map((c, i) => {
262
+ const shared = { id: c.id, regressed: ebh.rejected[i]!, evalue: evalues[i]!, logE: logEs[i]! };
263
+ if (c.paired) {
264
+ const p = c.paired;
265
+ const pairs = p.bothPass + p.worse + p.better + p.bothFail;
266
+ return {
267
+ ...shared,
268
+ ceiling: Infinity,
269
+ undetectable: false,
270
+ impossible: false,
271
+ observed: { successes: p.bothPass + p.better, trials: pairs },
272
+ baseline: { successes: p.bothPass + p.worse, trials: pairs },
273
+ design: "paired",
274
+ paired: { ...p },
275
+ };
276
+ }
277
+ // The ceiling is evaluated at the rate a regression would actually sit at,
278
+ // one MDE below the baseline's own rate. That is the alternative the gate is
279
+ // trying to detect, so it is the right place to ask "could this ever clear
280
+ // the bar". Matches how plan.ts prices the same case.
281
+ const bRate = c.baseline!.successes / c.baseline!.trials;
282
+ const pAlt = Math.max(1e-6, Math.min(1 - 1e-6, bRate - opts.mde));
283
+ const ceiling = impossible[i]
284
+ ? 1
285
+ : Math.exp(
286
+ ceilingLogEvidence(
287
+ opts.evidence,
288
+ pAlt,
289
+ c.baseline!.successes,
290
+ c.baseline!.trials,
291
+ opts.mde,
292
+ opts.altConcentration,
293
+ ),
294
+ );
295
+ return {
296
+ ...shared,
297
+ ceiling,
298
+ undetectable: !impossible[i] && ceiling < solo,
299
+ impossible: impossible[i]!,
300
+ observed: { successes: c.successes, trials: c.trials },
301
+ baseline: { successes: c.baseline!.successes, trials: c.baseline!.trials },
302
+ design: "unpaired",
303
+ };
304
+ });
305
+
306
+ const regressed = verdicts.filter((v) => v.regressed);
307
+ const blind = verdicts.filter((v) => v.undetectable && !v.regressed);
308
+ const untested = verdicts.filter((v) => v.impossible);
309
+
310
+ const caveats: string[] = [];
311
+ if (blind.length > 0) {
312
+ caveats.push(
313
+ `${blind.length} of ${m} gated case(s) have a baseline too thin to certify a ` +
314
+ `${(opts.mde * 100).toFixed(0)}pt drop at any candidate budget (${blind
315
+ .slice(0, 3)
316
+ .map((v) => v.id)
317
+ .join(", ")}` +
318
+ `${blind.length > 3 ? ", …" : ""}), which needs more baseline runs, not more candidate runs`,
319
+ );
320
+ }
321
+ if (untested.length > 0) {
322
+ caveats.push(
323
+ `${untested.length} case(s) pass at or below ${(opts.mde * 100).toFixed(0)}% and cannot drop by ` +
324
+ `${(opts.mde * 100).toFixed(0)}pts, so they were not tested (${untested
325
+ .slice(0, 3)
326
+ .map((v) => v.id)
327
+ .join(", ")}` +
328
+ `${untested.length > 3 ? ", …" : ""})`,
329
+ );
330
+ }
331
+ if (newCases.length > 0) {
332
+ caveats.push(
333
+ `${newCases.length} case(s) have no baseline and were not gated ` +
334
+ `(${newCases.slice(0, 3).join(", ")}${newCases.length > 3 ? ", …" : ""})`,
335
+ );
336
+ }
337
+
338
+ const headline =
339
+ (regressed.length === 0
340
+ ? `PASS: no case cleared the bar of ${solo.toFixed(0)} at ${(opts.fdr * 100).toFixed(0)}% FDR over ${m} gated case(s)`
341
+ : `FAIL: ${regressed.length} of ${m} gated case(s) certified as regressed at ${(opts.fdr * 100).toFixed(0)}% FDR ` +
342
+ `(${regressed
343
+ .slice(0, 3)
344
+ .map((v) => v.id)
345
+ .join(", ")}${regressed.length > 3 ? ", …" : ""})`) +
346
+ (caveats.length > 0 ? `. Read with care: ${caveats.join("; ")}.` : "");
347
+
348
+ return {
349
+ verdict: regressed.length === 0 ? "PASS" : "FAIL",
350
+ cases: verdicts,
351
+ regressed,
352
+ newCases,
353
+ threshold: ebh.threshold,
354
+ soloThreshold: solo,
355
+ options: opts,
356
+ headline,
357
+ };
358
+ }
package/src/index.ts ADDED
@@ -0,0 +1,206 @@
1
+ /**
2
+ * peeksafe: statistically valid gates for non-deterministic eval suites.
3
+ *
4
+ * The problem this exists for: you run an eval suite, watch results arrive, stop
5
+ * a case when it looks decided, then correct for multiplicity with
6
+ * Benjamini-Hochberg. That construction has no error control. A p-value computed
7
+ * at a stopping boundary is not a p-value, and BH assumes it is.
8
+ *
9
+ * The library is organised in four layers, and most callers only need the first.
10
+ *
11
+ * ## 1. The decision
12
+ *
13
+ * gate(cases, options) which cases regressed, valid at any stopping time
14
+ *
15
+ * You bring counts. It brings e-values and e-BH. Nothing here knows or cares how
16
+ * you ran the evals, and nothing here will gate a case that has no baseline.
17
+ *
18
+ * ## 2. The budget
19
+ *
20
+ * makePlan(...) how many runs each case needs, and which are impossible
21
+ * computeFrontier(...) what is certifiable inside a budget
22
+ * affordabilityGrid(...) the same question as a grid over two axes
23
+ *
24
+ * These answer "how many runs do I need" before you spend anything, including
25
+ * the case where the honest answer is that no candidate budget will do and only
26
+ * more baseline runs will.
27
+ *
28
+ * ## 3. The check
29
+ *
30
+ * typeOneError(...) the error rate the gate delivers at rates you name
31
+ * certifyProbability(...) the same computation with separate baseline and candidate rates
32
+ * pairedCertifyProbability(...) the power of a paired design
33
+ *
34
+ * These are exact, not simulated, so a caller can check the gate on their own
35
+ * rates before trusting a verdict.
36
+ *
37
+ * ## 4. The statistics
38
+ *
39
+ * Everything the first two layers are built from, exported so a caller can check
40
+ * the arithmetic, build a different gate, or use one piece on its own: special
41
+ * functions, intervals, the SPRT, e-values, e-BH, power, paired designs, and the
42
+ * cluster-robust suite summary.
43
+ *
44
+ * Cases in a suite are not independent draws. Several cases generated from one
45
+ * source file share a failure mode, so a change that breaks the file moves all
46
+ * of them together. `clusteredEffect` and the rest of `cluster` are what stop
47
+ * the suite-level interval from being too narrow for that reason.
48
+ */
49
+
50
+ export {
51
+ type BaselineStat,
52
+ baselineNullRate,
53
+ type CaseRef,
54
+ toBaselineMap,
55
+ } from "./baseline.js";
56
+ export {
57
+ type CertifyProbabilityOptions,
58
+ certifyProbability,
59
+ type PairedCertifyProbabilityOptions,
60
+ pairedCertifyProbability,
61
+ type TypeOneErrorOptions,
62
+ typeOneError,
63
+ } from "./check.js";
64
+ export {
65
+ type ClusteredEffect,
66
+ type ClusteredMean,
67
+ type ClusterKeyDiagnostic,
68
+ type ClusterObservation,
69
+ caseFamily,
70
+ clusteredEffect,
71
+ clusterKey,
72
+ clusterKeyDiagnostic,
73
+ clusterRobustMean,
74
+ compareEstimators,
75
+ type FamilyEffect,
76
+ type GroupingSummary,
77
+ groupByCluster,
78
+ iidMean,
79
+ MIN_TRUSTWORTHY_CLUSTERS,
80
+ type RandomEffectsMean,
81
+ randomEffectsMean,
82
+ } from "./cluster.js";
83
+ /* ── errors and determinism ──────────────────────────────────────────────── */
84
+ export {
85
+ MAX_ALT_CONCENTRATION,
86
+ PeeksafeError,
87
+ type PeeksafeErrorCode,
88
+ type PeeksafeErrorOptions,
89
+ } from "./errors.js";
90
+ export type { Evidence } from "./evidence.js";
91
+ export {
92
+ computeFrontier,
93
+ costShares,
94
+ DEFAULT_AXES,
95
+ DEFAULT_FRONTIER,
96
+ enumerateFrontier,
97
+ evaluatePoint,
98
+ type FrontierAxes,
99
+ type FrontierBasis,
100
+ type FrontierCell,
101
+ type FrontierConfig,
102
+ type FrontierCost,
103
+ type FrontierDesign,
104
+ type FrontierPoint,
105
+ type FrontierResult,
106
+ type FrontierScreen,
107
+ fmtRate,
108
+ typicalObservationsPerCase,
109
+ } from "./frontier.js";
110
+ /* ── 1. the decision ─────────────────────────────────────────────────────── */
111
+ export {
112
+ type CaseVerdict,
113
+ DEFAULT_GATE_OPTIONS,
114
+ type GateCase,
115
+ type GateOptions,
116
+ type GateResult,
117
+ gate,
118
+ type PairedGateCase,
119
+ } from "./gate.js";
120
+ /* ── 2. the budget ───────────────────────────────────────────────────────── */
121
+ export {
122
+ type AffordabilityCell,
123
+ affordabilityGrid,
124
+ DEFAULT_PLAN,
125
+ type Detectability,
126
+ expectedSequentialSamples,
127
+ makePlan,
128
+ type Plan,
129
+ type PlanCase,
130
+ type PlanConfig,
131
+ type PlanTotals,
132
+ planCase,
133
+ } from "./plan.js";
134
+ export { hash32, makeRand, type Rand, streamFor } from "./rand.js";
135
+ /* ── 3. the statistics ───────────────────────────────────────────────────── */
136
+ export {
137
+ type BetaPosterior,
138
+ type BhResult,
139
+ bernoulliEntropy,
140
+ betaCredibleInterval,
141
+ betaMassBetween,
142
+ betaPosterior,
143
+ betaQuantile,
144
+ /* multiplicity */
145
+ bhCorrect,
146
+ cohensH,
147
+ diffInterval,
148
+ discordant,
149
+ type EbhResult,
150
+ ebhCorrect,
151
+ ebhSoloThreshold,
152
+ erf,
153
+ evidenceCeilingAsymptotic,
154
+ evidenceCeilingLogE,
155
+ evidenceCeilingSlope,
156
+ expectedLogE,
157
+ expectedLogEExact,
158
+ fisherExact2x2,
159
+ gammaP,
160
+ type Interval,
161
+ ibeta,
162
+ logBeta,
163
+ logBetaPdf,
164
+ /* special functions */
165
+ logGamma,
166
+ /* e-values */
167
+ logMarginalBetaBinomial,
168
+ mcnemarSamplesForEvidence,
169
+ normalCdf,
170
+ normalQuantile,
171
+ type PairedCounts,
172
+ pairedDiscordance,
173
+ /* paired designs */
174
+ pairedLogE,
175
+ pairPhi,
176
+ probabilityMoved,
177
+ type SprtDecision,
178
+ type SprtOpts,
179
+ type SprtResult,
180
+ /* power */
181
+ sampleSizeTwoProportion,
182
+ samplesForEvidence,
183
+ /* sequential */
184
+ sprtDecision,
185
+ sprtExpectedN,
186
+ type TestResult,
187
+ /* fixed-sample tests, for comparison rather than for gating */
188
+ twoProportionZTest,
189
+ twoSampleLogE,
190
+ twoSamplePriors,
191
+ universalCeilingLogE,
192
+ universalTwoSampleLogE,
193
+ /* intervals and posteriors */
194
+ wilsonInterval,
195
+ } from "./stats.js";
196
+ export {
197
+ DEFAULT_STOP_OPTIONS,
198
+ type PairedStopDecision,
199
+ type PairedStopOptions,
200
+ type PairedStopReason,
201
+ type StopDecision,
202
+ type StopOptions,
203
+ type StopReason,
204
+ shouldStop,
205
+ shouldStopPaired,
206
+ } from "./stop.js";