retrieval-eval-gate 0.1.0__tar.gz

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 (31) hide show
  1. retrieval_eval_gate-0.1.0/LICENSE +21 -0
  2. retrieval_eval_gate-0.1.0/PKG-INFO +376 -0
  3. retrieval_eval_gate-0.1.0/README.md +325 -0
  4. retrieval_eval_gate-0.1.0/pyproject.toml +65 -0
  5. retrieval_eval_gate-0.1.0/setup.cfg +4 -0
  6. retrieval_eval_gate-0.1.0/src/rag_eval_gate/__init__.py +39 -0
  7. retrieval_eval_gate-0.1.0/src/rag_eval_gate/audit.py +257 -0
  8. retrieval_eval_gate-0.1.0/src/rag_eval_gate/backends.py +253 -0
  9. retrieval_eval_gate-0.1.0/src/rag_eval_gate/cli.py +403 -0
  10. retrieval_eval_gate-0.1.0/src/rag_eval_gate/conformal.py +444 -0
  11. retrieval_eval_gate-0.1.0/src/rag_eval_gate/goldset.py +136 -0
  12. retrieval_eval_gate-0.1.0/src/rag_eval_gate/harness.py +281 -0
  13. retrieval_eval_gate-0.1.0/src/rag_eval_gate/metrics.py +106 -0
  14. retrieval_eval_gate-0.1.0/src/rag_eval_gate/power.py +154 -0
  15. retrieval_eval_gate-0.1.0/src/rag_eval_gate/ppi.py +224 -0
  16. retrieval_eval_gate-0.1.0/src/rag_eval_gate/report.py +160 -0
  17. retrieval_eval_gate-0.1.0/src/rag_eval_gate/sequential.py +312 -0
  18. retrieval_eval_gate-0.1.0/src/rag_eval_gate/stats.py +415 -0
  19. retrieval_eval_gate-0.1.0/src/rag_eval_gate/version.py +3 -0
  20. retrieval_eval_gate-0.1.0/src/retrieval_eval_gate.egg-info/PKG-INFO +376 -0
  21. retrieval_eval_gate-0.1.0/src/retrieval_eval_gate.egg-info/SOURCES.txt +29 -0
  22. retrieval_eval_gate-0.1.0/src/retrieval_eval_gate.egg-info/dependency_links.txt +1 -0
  23. retrieval_eval_gate-0.1.0/src/retrieval_eval_gate.egg-info/entry_points.txt +3 -0
  24. retrieval_eval_gate-0.1.0/src/retrieval_eval_gate.egg-info/requires.txt +4 -0
  25. retrieval_eval_gate-0.1.0/src/retrieval_eval_gate.egg-info/top_level.txt +1 -0
  26. retrieval_eval_gate-0.1.0/tests/test_audit.py +195 -0
  27. retrieval_eval_gate-0.1.0/tests/test_conformal.py +403 -0
  28. retrieval_eval_gate-0.1.0/tests/test_pipeline.py +501 -0
  29. retrieval_eval_gate-0.1.0/tests/test_ppi.py +225 -0
  30. retrieval_eval_gate-0.1.0/tests/test_sequential.py +276 -0
  31. retrieval_eval_gate-0.1.0/tests/test_stats.py +296 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jinwovo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,376 @@
1
+ Metadata-Version: 2.4
2
+ Name: retrieval-eval-gate
3
+ Version: 0.1.0
4
+ Summary: Retrieval evaluation that reports what it cannot resolve — intervals, significance, anytime-valid CI gates, and distribution-free guarantees. Zero dependencies, any retriever.
5
+ Author-email: Jinwoo Kwon <kownjinwoo@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 jinwovo
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/jinwovo/rag-eval-gate
29
+ Project-URL: Source, https://github.com/jinwovo/rag-eval-gate
30
+ Project-URL: Issues, https://github.com/jinwovo/rag-eval-gate/issues
31
+ Keywords: rag,retrieval,evaluation,information-retrieval,benchmark,conformal-prediction,anytime-valid,confidence-sequence,ci-gate,beir
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Intended Audience :: Science/Research
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
42
+ Classifier: Topic :: Software Development :: Testing
43
+ Classifier: Typing :: Typed
44
+ Requires-Python: >=3.10
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest>=7; extra == "dev"
49
+ Requires-Dist: scipy>=1.10; extra == "dev"
50
+ Dynamic: license-file
51
+
52
+ # rag-eval-gate
53
+
54
+ **Retrieval evaluation that tells you what it can't resolve.**
55
+
56
+ [![ci](https://github.com/jinwovo/rag-eval-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/jinwovo/rag-eval-gate/actions/workflows/ci.yml)
57
+ [![python](https://img.shields.io/badge/python-3.10%20%E2%80%93%203.13-blue)](https://github.com/jinwovo/rag-eval-gate/actions/workflows/ci.yml)
58
+ [![dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)](#why-zero-dependencies)
59
+ [![license](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE)
60
+
61
+ > `Recall@5 = 1.00` on 10 queries is really **`[0.69, 1.00]`**.
62
+ > That `+0.30` lift over BM25 has a p-value floor of **0.25** — no outcome of that
63
+ > comparison could ever have been significant.
64
+ > Both of those are computable **from the published table alone.**
65
+
66
+ ## → [Check a number right now, nothing to install](https://jinwovo.github.io/rag-eval-gate/)
67
+
68
+ Type in the figure and the query count. It returns the interval that figure supports and,
69
+ for a claimed improvement, whether *any* outcome of that comparison could have reached
70
+ significance. The arithmetic runs in the page; nothing is sent anywhere.
71
+
72
+ Or from a terminal:
73
+
74
+ ```bash
75
+ pip install retrieval-eval-gate # the PyPI name; `rag-eval-gate` there is someone else's
76
+
77
+ rag-eval-gate audit -n 10 -v 1.00 -b 0.70 # audit a table you are reading
78
+ rag-eval-gate power gold.jsonl # what your own gold set can resolve
79
+ ```
80
+
81
+ Zero dependencies. Works with any retriever in any language. Runs as a GitHub Action.
82
+ The command is `rag-eval-gate`; the distribution is `retrieval-eval-gate` because the shorter
83
+ name on PyPI belongs to an unrelated project. Both spellings work as the command.
84
+
85
+ <picture>
86
+ <source media="(prefers-color-scheme: dark)" srcset="docs/anytime-dark.png">
87
+ <img src="docs/anytime-light.png" alt="Left: a fixed-N 95% confidence interval, checked after every query, misses the true score in 17% of runs at 50 queries and 33% at 300, against 0.5-1.7% for an anytime-valid confidence sequence. Right: a sequential gate spends 31 of its 300-query budget on a clearly broken system and 54 on a clearly good one, but the full budget on one sitting exactly at the threshold.">
88
+ </picture>
89
+
90
+ ---
91
+
92
+ ## Three things your eval is probably not telling you
93
+
94
+ **A perfect score is not a perfect score.** Ten queries, one relevant document each,
95
+ `Recall@5 = 1.00` — that is ten successes out of ten, and the exact binomial interval
96
+ bottoms out at `(α/2)^(1/n) = 0.69`. The headline is 31 points wide.
97
+
98
+ **Some comparisons cannot be won.** Your hybrid retriever beats BM25 by `+0.30 Recall@5`.
99
+ On ten queries that is three queries flipping, seven tying — and a paired randomization
100
+ test on three differing pairs has a smallest attainable two-sided p of `2/2³ = 0.25`. Not
101
+ "not significant": *unable* to be, whatever the result.
102
+
103
+ **Your interval breaks when you look at it.** A 95% interval promises that **one** look at
104
+ a **finished** sample contains the truth 95% of the time. Watching a CI log is not that.
105
+ Over 800 simulated evaluation streams, inspection starting only at n = 30 where the normal
106
+ approximation is defensible:
107
+
108
+ | metric | queries | fixed-N interval | confidence sequence |
109
+ |---|:---:|:---:|:---:|
110
+ | Recall@5 (0/1) | 300 | **34.0%** | 1.9% |
111
+ | reciprocal rank (discrete, skewed) | 300 | **32.8%** | 2.6% |
112
+ | nDCG@10 (graded) | 300 | **35.1%** | 1.6% |
113
+ | groundedness judge (skewed high) | 300 | **30.0%** | 0.5% |
114
+
115
+ *Runs where the true mean escapes a nominal 95% interval at some point while it is watched.*
116
+ The fixed-N column grows with stream length — the longer you look, the more chances to be
117
+ wrong. That is the law of the iterated logarithm, and it is why the right-hand column exists.
118
+
119
+ ## Audit a table in five seconds
120
+
121
+ No gold set, no retriever, no data — the reported means and the query count are enough:
122
+
123
+ ```console
124
+ $ rag-eval-gate audit -n 10 -m recall@5 -v 1.00 -b 0.70
125
+
126
+ recall@5 = 1.000 on 10 queries
127
+ that is 10/10 successes
128
+
129
+ 95% interval [0.692, 1.000] width 0.308 (exact binomial)
130
+ The reported figure is 31 points wide. Quoting it to two decimal places implies a
131
+ precision 10 queries cannot deliver.
132
+
133
+ Claimed lift +0.300 over 0.700
134
+ note: the two means determine it exactly: 3 of 10 queries differ
135
+
136
+ smallest attainable p 0.2500
137
+
138
+ UNRESOLVABLE. Under every possible overlap of the two systems' successes,
139
+ a paired test on this many differing queries has a floor above 0.05, so no
140
+ outcome of that comparison could have reached significance. This is a fact
141
+ about the sample size, not about the systems.
142
+ ```
143
+
144
+ It exits non-zero when the comparison could not have been significant, so it works as a
145
+ check and not only as something to read. The interval is exact for a 0/1 metric; for a
146
+ graded one the per-query spread is unknown, so it reports the widest interval the mean
147
+ allows — a `[0,1]` variable with mean *m* has variance at most *m*(1−*m*).
148
+
149
+ The same thing runs in a browser at
150
+ [jinwovo.github.io/rag-eval-gate](https://jinwovo.github.io/rag-eval-gate/), with a
151
+ shareable link for the result.
152
+
153
+ ## Quickstart
154
+
155
+ ```bash
156
+ pip install retrieval-eval-gate
157
+
158
+ # 1. What can this gold set even measure? (no retriever needed)
159
+ rag-eval-gate power gold.jsonl
160
+
161
+ # 2. Score it, with intervals and significance
162
+ rag-eval-gate eval gold.jsonl --modes bm25,vector,hybrid
163
+
164
+ # 3. Gate a pull request
165
+ rag-eval-gate eval gold.jsonl --gate --min-mrr10 0.85
166
+ ```
167
+
168
+ A gold set is JSONL:
169
+
170
+ ```json
171
+ {"query": "how do I rotate a signing key", "relevant_doc_ids": ["kms-rotation"]}
172
+ ```
173
+
174
+ …or the TREC qrels you already have (`--gold qrels/test.tsv --queries-file queries.jsonl`).
175
+
176
+ ## Works with whatever you built
177
+
178
+ Three ways in, so the language and shape of your retriever are irrelevant.
179
+
180
+ **HTTP** — a URL template and a small field map. No assumption about the route or schema:
181
+
182
+ ```bash
183
+ rag-eval-gate eval gold.jsonl \
184
+ --url-template 'https://search.internal/v2/query?text={query}&strategy={mode}' \
185
+ --results-path 'data.hits' --id-field 'document_id' --score-field 'relevance'
186
+ ```
187
+
188
+ **A command** — anything that prints JSON to stdout. Go, Rust, a notebook, a shell script:
189
+
190
+ ```bash
191
+ rag-eval-gate eval gold.jsonl \
192
+ --backend command --command './retrieve --q {query} --mode {mode} --json'
193
+ ```
194
+
195
+ **A TREC run file** — rank the corpus once, offline, on whatever hardware; evaluate as often
196
+ as you like with nothing running:
197
+
198
+ ```bash
199
+ rag-eval-gate eval qrels/test.tsv --backend run-file --run-file runs/bm25.trec
200
+ ```
201
+
202
+ ## Four gate policies
203
+
204
+ ```bash
205
+ rag-eval-gate eval gold.jsonl --gate --gate-policy sequential
206
+ ```
207
+
208
+ | policy | fails when | use it when |
209
+ |---|---|---|
210
+ | `point` *(default)* | the mean falls below the threshold | always — the absolute floor |
211
+ | `ci-lower` | the 95% lower bound falls below it | your gold set is big enough that the interval is narrower than the safety margin |
212
+ | `regression` | *also* on a significant paired drop against a recorded run | you have a green baseline to compare against |
213
+ | `sequential` | the anytime-valid verdict is `fail`, or the budget runs out `undecided` | queries cost money or minutes |
214
+
215
+ `regression` is the sensitive one. An absolute threshold only notices a regression once the
216
+ mean crosses a line someone guessed; a paired test compares the **same queries** before and
217
+ after, so a real drop on two of them is caught while the mean still clears the line.
218
+
219
+ `sequential` is the cheap one. Because a confidence sequence is valid at every sample size,
220
+ stopping as soon as the verdict is settled is a decision rather than a peek:
221
+
222
+ | the system's true Recall@5 | verdict | queries scored (of 300) | saved |
223
+ |:---:|:---:|:---:|:---:|
224
+ | 0.98 | pass | 54 | **82%** |
225
+ | 0.95 | pass | 89 | **70%** |
226
+ | 0.85 (exactly at the line) | `undecided` | 293 | 2% |
227
+ | 0.60 | fail | 31 | **90%** |
228
+
229
+ `undecided` fails. The budget ran out before the evidence arrived, which is a fact about the
230
+ gold set rather than a clean bill of health — and a gate that treats "we could not tell" as
231
+ a pass has quietly stopped gating.
232
+
233
+ ## As a GitHub Action
234
+
235
+ ```yaml
236
+ - uses: jinwovo/rag-eval-gate@v1
237
+ with:
238
+ gold-file: eval/gold.jsonl
239
+ url-template: 'http://localhost:8080/api/search?q={query}&mode={mode}'
240
+ gate-policy: sequential
241
+ min-mrr10: "0.85"
242
+ ```
243
+
244
+ The step summary gets the interval table, the significance verdicts, what the gold set can
245
+ resolve, and a ✅/❌.
246
+
247
+ ## What comes out
248
+
249
+ ```
250
+ queries=300 depth=10 intervals=95%
251
+
252
+ mode recall@5 recall@10 mrr@10 ndcg@10
253
+ --------------------------------------------------------------------------------------------------
254
+ bm25 0.700 [0.35,0.93] 0.800 [0.44,0.97] 0.670 [0.35,0.90] 0.702 [0.38,0.90]
255
+ hybrid 1.000 [0.69,1.00] 1.000 [0.69,1.00] 0.950 [0.70,1.00] 0.963 [0.78,1.00]
256
+
257
+ paired randomization test vs bm25 (Holm-corrected within each metric)
258
+ recall@5 hybrid delta=+0.300 p=0.2500 holm=0.2500 unresolvable (3/10 queries differ; floor p = 0.250)
259
+ mrr@10 hybrid delta=+0.280 p=0.1250 holm=0.2500 unresolvable (4/10 queries differ; floor p = 0.125)
260
+
261
+ resolution of this gold set (hybrid vs bm25, mrr@10):
262
+ n = 10 queries, per-query difference sd = 0.388
263
+ smallest detectable effect at 80% power: +0.344
264
+ queries needed to resolve +0.020: 2,957
265
+ ```
266
+
267
+ `unresolvable` is the word this tool exists to print. It is not a softer way of saying "not
268
+ significant" — it means the queries that differ are too few for *any* outcome to reach
269
+ p < 0.05, which is a property of your labels and no amount of rerunning will change it.
270
+
271
+ ## The statistics, and why each one
272
+
273
+ | | what | why not the obvious thing |
274
+ |---|---|---|
275
+ | **Exact binomial** (Clopper–Pearson) | intervals for Recall@k when each query has one relevant document | a bootstrap resamples a constant at 10/10 and returns `[1.00, 1.00]`, which reads as certainty |
276
+ | **BCa bootstrap** | intervals for graded metrics | percentile intervals under-cover on skewed samples, and reciprocal rank is very skewed |
277
+ | **Paired randomization test** <br><sub>Smucker, Allan & Carterette, CIKM 2007</sub> | mode comparisons | reciprocal rank is discrete, bounded and skewed; a t-test at n = 10 is not measuring what it claims |
278
+ | **Holm–Bonferroni** | correction across a mode sweep | five challengers against one baseline is five chances at a false win; uncorrected 0.05 is about 0.23 |
279
+ | **Betting confidence sequences** <br><sub>Waudby-Smith & Ramdas, JRSS-B 2024</sub> | the sequential gate | alpha spending needs the looks planned in advance; a CI log is inspected continuously |
280
+ | **Conformal prediction, adaptive sets** <br><sub>Romano, Sesia & Candès, NeurIPS 2020</sub> | `calibrate`: how many passages to send an LLM | a fixed top-K over-spends on easy queries and truncates the hard ones |
281
+ | **Risk-controlling prediction sets** <br><sub>Bates et al., JACM 2021</sub> | `calibrate`: when to abstain | a hand-picked threshold implies a claim about hallucination rate that was never checked |
282
+ | **Prediction-powered inference** <br><sub>Angelopoulos et al., Science 2023</sub> | `ppi_mean`: a valid interval when only a biased LLM judge scored your data | averaging the judge gives a tight interval around the judge's belief, which covers the truth ~0% of the time |
283
+
284
+ Every one is seeded, so a gate cannot flap because a resample drew differently.
285
+
286
+ ## Certified thresholds
287
+
288
+ ```bash
289
+ rag-eval-gate calibrate gold.jsonl --alpha 0.10 --risk-alpha 0.05
290
+ ```
291
+
292
+ Turns the two constants that decide what a RAG system costs and how often it can make things
293
+ up into thresholds with finite-sample, distribution-free guarantees:
294
+
295
+ ```
296
+ coverage P(the passages sent to the LLM contain a relevant document) >= 1 - alpha
297
+ risk P( P(answering from context with no relevant document) <= risk-alpha ) >= 1 - delta
298
+ ```
299
+
300
+ Measured on 800 queries against a stand-in cross-encoder: **5.0 passages against a fixed 9
301
+ carrying the identical promise — 44% fewer prompt tokens**, because the budget moves to
302
+ where it is needed (3.2 passages when the answer ranked first, 8.4 when it ranked fourth or
303
+ lower). Three splits — one picks the softmax temperature, one fits the quantile, one scores
304
+ both having informed neither — because choosing the temperature on the data the quantile is
305
+ fitted on breaks exchangeability, and the resulting under-coverage is invisible in every
306
+ in-sample number.
307
+
308
+ A certificate that abstains on everything is valid, useless, and labelled **degenerate**. A
309
+ threshold search that stalls names the calibration size that would settle it.
310
+
311
+ ## When only an LLM has judged your data
312
+
313
+ Your groundedness score is a judge model's opinion of the system it belongs to. Your
314
+ relevance labels, at any real scale, were written by a model too. Both are cheap and neither
315
+ is trustworthy on its own, and averaging thousands of them does not produce a 95% interval —
316
+ it produces a very narrow interval around whatever the model believes.
317
+
318
+ `ppi_mean` implements **prediction-powered inference** ([Angelopoulos et al., *Science*
319
+ 2023](https://www.science.org/doi/10.1126/science.adi6000), power-tuned as PPI++): hand-label
320
+ a small sample, let the model predict everything, and subtract the bias measured on the
321
+ labelled part.
322
+
323
+ ```python
324
+ from rag_eval_gate import ppi_mean
325
+
326
+ estimate = ppi_mean(hand_labels, judge_on_those, judge_on_everything_else)
327
+ print(estimate.statement())
328
+ # The judge reports 0.886. Measured against 50 hand labels it is optimistic by 0.081,
329
+ # so the true value is 0.805 [0.781, 0.829] — an interval worth about 474 hand labels,
330
+ # from 50.
331
+ ```
332
+
333
+ Over 1,500 repetitions of the whole label-and-estimate cycle, 50 hand labels against 2,050
334
+ judge-scored items:
335
+
336
+ | judge behaviour | coverage: judge only | hand labels only | **PPI** | interval width | effective labels *(from 50)* |
337
+ |---|:---:|:---:|:---:|:---:|:---:|
338
+ | flattering, accurate | **0.0%** | 94.0% | **94.3%** | −67% | **474** |
339
+ | flattering, noisy | **0.0%** | 94.2% | **93.5%** | −22% | 84 |
340
+ | unbiased, noisy | 14.5% | 94.7% | **93.7%** | −22% | 85 |
341
+ | uninformative | **0.0%** | 94.8% | **94.3%** | −1% | 51 |
342
+
343
+ **Validity never depends on the judge being good** — the bias is measured, not assumed. And
344
+ **a useless judge costs nothing**: λ is tuned to minimise variance, so noise gets λ ≈ 0 and
345
+ the estimator falls back to the hand-label mean. That last row is the safety property, not a
346
+ failure.
347
+
348
+ ## Why zero dependencies
349
+
350
+ This runs inside a CI job that should not need a resolver, a wheel build or a lockfile to
351
+ gate a pull request. The machinery it needs — a regularized incomplete beta, a BCa
352
+ bootstrap, a martingale — is a few hundred readable lines, and `pip install retrieval-eval-gate`
353
+ installs exactly one thing. The test suite cross-checks the hand-rolled special functions
354
+ against SciPy, and skips those checks when SciPy is not installed.
355
+
356
+ ## Tests
357
+
358
+ ```bash
359
+ pip install -e ".[dev]" && pytest
360
+ ```
361
+
362
+ 200 tests. Against closed forms (`(α/2)^(1/n)` at `k = n`, `2^(1-k)` for a uniform improvement, R's
363
+ `p.adjust` worked example, the `(n+1)` conformal quantile correction), against an
364
+ independent brute-force permutation reference written inside the test, and — for the
365
+ guarantees — by simulating the whole calibrate-then-deploy cycle and counting how often each
366
+ promise actually breaks.
367
+
368
+ ## Provenance
369
+
370
+ Extracted from [jinwovo/recall](https://github.com/jinwovo/recall), a hybrid-search and
371
+ grounded-RAG system, after its own eval harness was pointed at its own README and three of
372
+ the headline claims failed. The design notes live there as ADRs 0011–0014.
373
+
374
+ ## License
375
+
376
+ MIT.