evalproof 0.2.1__tar.gz → 0.3.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 (68) hide show
  1. {evalproof-0.2.1 → evalproof-0.3.0}/CHANGELOG.md +80 -3
  2. {evalproof-0.2.1 → evalproof-0.3.0}/CITATION.cff +2 -2
  3. {evalproof-0.2.1 → evalproof-0.3.0}/PKG-INFO +128 -15
  4. {evalproof-0.2.1 → evalproof-0.3.0}/README.md +128 -15
  5. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/__init__.py +5 -1
  6. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/_version.py +1 -1
  7. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/cache.py +10 -2
  8. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/cli.py +65 -4
  9. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/config.py +48 -6
  10. evalproof-0.3.0/src/evalproof/gate.py +135 -0
  11. evalproof-0.3.0/src/evalproof/importers/__init__.py +75 -0
  12. evalproof-0.3.0/src/evalproof/importers/_common.py +309 -0
  13. evalproof-0.3.0/src/evalproof/importers/deepeval.py +127 -0
  14. evalproof-0.3.0/src/evalproof/importers/inspect_ai.py +207 -0
  15. evalproof-0.3.0/src/evalproof/importers/lm_eval.py +114 -0
  16. evalproof-0.3.0/src/evalproof/importers/promptfoo.py +256 -0
  17. evalproof-0.3.0/src/evalproof/importers/table.py +246 -0
  18. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/providers/__init__.py +32 -7
  19. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/providers/anthropic.py +38 -11
  20. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/providers/base.py +27 -0
  21. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/providers/ollama.py +10 -2
  22. evalproof-0.3.0/src/evalproof/providers/openai.py +163 -0
  23. evalproof-0.3.0/src/evalproof/providers/profiles.py +164 -0
  24. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/report.py +23 -12
  25. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/stats.py +20 -0
  26. evalproof-0.3.0/tests/fixtures/README.md +9 -0
  27. evalproof-0.3.0/tests/fixtures/deepeval/test_run.json +167 -0
  28. evalproof-0.3.0/tests/fixtures/inspect/log_formats.json +399 -0
  29. evalproof-0.3.0/tests/fixtures/lm-eval/gpt2/samples_hellaswag_2024-05-01T10-20-30.123456.jsonl +4 -0
  30. evalproof-0.3.0/tests/fixtures/lm-eval/llama3/samples_gsm8k_2024-05-01T10-20-30.123456.jsonl +6 -0
  31. evalproof-0.3.0/tests/fixtures/promptfoo/sample-export.json +692 -0
  32. evalproof-0.3.0/tests/fixtures/table/long.csv +9 -0
  33. evalproof-0.3.0/tests/fixtures/table/long.jsonl +8 -0
  34. evalproof-0.3.0/tests/fixtures/table/wide.csv +4 -0
  35. evalproof-0.3.0/tests/test_action.py +85 -0
  36. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_cache_systems.py +22 -0
  37. evalproof-0.3.0/tests/test_holm_gate.py +168 -0
  38. evalproof-0.3.0/tests/test_importers.py +795 -0
  39. evalproof-0.3.0/tests/test_providers.py +517 -0
  40. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_public_api.py +14 -0
  41. evalproof-0.3.0/tests/test_release_fixes.py +162 -0
  42. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_security.py +23 -9
  43. evalproof-0.2.1/src/evalproof/gate.py +0 -107
  44. evalproof-0.2.1/src/evalproof/providers/openai.py +0 -76
  45. evalproof-0.2.1/tests/test_providers.py +0 -161
  46. {evalproof-0.2.1 → evalproof-0.3.0}/.gitignore +0 -0
  47. {evalproof-0.2.1 → evalproof-0.3.0}/LICENSE +0 -0
  48. {evalproof-0.2.1 → evalproof-0.3.0}/NOTICE +0 -0
  49. {evalproof-0.2.1 → evalproof-0.3.0}/pyproject.toml +0 -0
  50. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/calibration.py +0 -0
  51. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/compare.py +0 -0
  52. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/dataset.py +0 -0
  53. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/models.py +0 -0
  54. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/py.typed +0 -0
  55. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/runner.py +0 -0
  56. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/scorers/__init__.py +0 -0
  57. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/scorers/deterministic.py +0 -0
  58. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/scorers/judge.py +0 -0
  59. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/scorers/retrieval.py +0 -0
  60. {evalproof-0.2.1 → evalproof-0.3.0}/src/evalproof/systems.py +0 -0
  61. {evalproof-0.2.1 → evalproof-0.3.0}/tests/__init__.py +0 -0
  62. {evalproof-0.2.1 → evalproof-0.3.0}/tests/conftest.py +0 -0
  63. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_calibration.py +0 -0
  64. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_compare_gate_report.py +0 -0
  65. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_config_dataset.py +0 -0
  66. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_runner_cli.py +0 -0
  67. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_scorers.py +0 -0
  68. {evalproof-0.2.1 → evalproof-0.3.0}/tests/test_stats.py +0 -0
@@ -2,6 +2,81 @@
2
2
 
3
3
  All notable changes are listed here. The project follows [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.3.0] — 2026-11-12
8
+
9
+ ### Added
10
+
11
+ - `evalproof import` and `evalproof.import_run`: per-case results from promptfoo (JSON and
12
+ JSONL), Inspect (`.json` and `.eval` logs), DeepEval, lm-evaluation-harness
13
+ (`samples_*.jsonl`) and generic long or wide `jsonl` and `csv` files become a run that
14
+ `compare` and the gate read. Case ids are stable across runs, system errors score as
15
+ failures, repeats collapse by `--epochs`, and NaN or infinite scores are refused with the
16
+ case and metric named. Raises `evalproof.ImportFormatError` (a `ValueError`; exit 2 on the
17
+ command line).
18
+ - Gating several metrics: `gate.metrics` in the suite or `--metric` repeated on
19
+ `compare`, with Holm-adjusted p-values (`evalproof.holm`). The decision carries
20
+ `adjusted_p`, the report gains a Holm-adjusted p column, and a single metric behaves as
21
+ before. ADR 0003 has the reasoning.
22
+ - The composite action's `metric` input takes several names, separated by newlines or commas.
23
+ - `examples/retrieval`: an offline retrieval example for recall@k, MRR and nDCG, with a
24
+ regression that recall@5 cannot see and MRR and nDCG@5 can. Contributed by
25
+ @HarshRajSinghania in #5. CI asserts its gate exits 1 on the regression and 0 on no change.
26
+ - MEASUREMENTS §8: the support-qa judge with thinking on (#3), with its runs, suite and
27
+ analysis in `measure.py`. Against labels audited case by case it passes no wrong answer
28
+ and κ rises, at 6.7× the judge time; the gate's decision does not change.
29
+ - A provider profile table (`providers/profiles.py`) behind one OpenAI-compatible adapter:
30
+ `openai`, `azure`, `gemini`, `mistral`, `groq`, `together`, `fireworks`, `deepseek`, `xai`,
31
+ `openrouter`, `cerebras`, the local servers `vllm`, `lmstudio` and `llamacpp`, and
32
+ `openai-compatible` for any other server (set `base_url`). Each profile carries its base
33
+ URL, key variable, auth header, token-limit field and a link to the provider's docs.
34
+ `OpenAIProvider` stays importable.
35
+ - `TargetConfig.api_key_env` names the environment variable that holds the key, and
36
+ `TargetConfig.headers` adds request headers to every HTTP provider; suite headers win over
37
+ a provider's own, and neither is part of the cache key or written to a run. A named key
38
+ variable that is not set is an error rather than a 401.
39
+ - Anthropic: a `refusal` stop reason is recorded as a case error naming its category, and so
40
+ is `max_tokens` reached before any text, with a hint to raise `params.max_tokens`. The
41
+ OpenAI-compatible adapter does the same for `finish_reason` `length` with no text, and
42
+ for `content_filter`, and reads replies that arrive as a list of typed content parts.
43
+ - A test pins the cache key of an existing Ollama call, so a change to it cannot slip in.
44
+
45
+ ### Changed
46
+
47
+ - Hosted providers (everything except Ollama and the local servers) send `temperature` and
48
+ `seed` only when the suite sets them. Before, `openai` sent `temperature: 0` and
49
+ `seed: 0` on every call, which reasoning models reject; now an unset value is left to the
50
+ provider's default. A hosted run that relied on the implicit `temperature: 0` should set
51
+ it in `params`. Ollama and the local servers still send both, so local runs repeat as
52
+ before and keep their cache keys; `GenerationParams` defaults are unchanged. A hosted call
53
+ that now leaves a value out gets a new cache key, so a reply cached at the old implicit
54
+ `temperature: 0` is not reused for it. To point `openai` at an older local server through
55
+ `base_url`, use `openai-compatible` or a local profile instead, which send both values.
56
+ - `openai` sends the token limit as `max_completion_tokens`, which OpenAI's reasoning models
57
+ require (they reject `max_tokens`), and `mistral` sends its seed as `random_seed`. The
58
+ default limit is still 512.
59
+ - The llama.cpp profile defaults to port 9931, the one its server documents now; for an older
60
+ build on 8080, set `base_url`.
61
+
62
+ ### Fixed
63
+
64
+ - Anthropic calls to current Claude models failed with HTTP 400, because `temperature` was
65
+ sent on every request. It goes out only when the suite sets it, and no seed is sent, since
66
+ the Messages API has none.
67
+ - A gate built without validation and given no metric passed by gating nothing; it now
68
+ cannot gate (exit 4), and `compare --metric ""` is a usage error (exit 2).
69
+ - Found in review before release, in the new importers: two promptfoo prompts whose first 60
70
+ characters matched merged into one system (the label is no longer truncated, and two
71
+ prompts that share a label are an error), promptfoo output with repeated tests but no
72
+ `repeatIndex` now warns that it reads as separate cases, Inspect message ids no longer
73
+ change the dataset hash, a `null` promptfoo `testIdx` no longer crashes, and under
74
+ `--epochs any` or `mean` a case with one good repeat is no longer marked as an error.
75
+ - `examples/support-qa/cases.jsonl`: the q21 and q22 references named Enterprise, which the
76
+ product facts do not give SSO or an admin console (#7). The original set is kept as
77
+ `cases.v1.jsonl` so the recorded runs stay reproducible; the runs judged on the revised set
78
+ are in `runs/v2/`, and MEASUREMENTS §8 reports both.
79
+
5
80
  ## [0.2.1] — 2026-10-16
6
81
 
7
82
  ### Fixed
@@ -26,7 +101,7 @@ The first release on PyPI: `pip install evalproof`.
26
101
  stored anywhere. Each file on PyPI carries a PEP 740 attestation, and the GitHub release
27
102
  carries the same files with a build-provenance attestation.
28
103
  - `SECURITY.md` with private vulnerability reporting and the trust boundaries, backed by a
29
- test that runs both hosted providers with a fake key and finds it in no file written.
104
+ test that runs both hosted providers with a placeholder key and finds it in no file written.
30
105
  - `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1), issue and pull-request templates,
31
106
  `CITATION.cff`, `CODEOWNERS`, Dependabot for actions and Python dependencies with a
32
107
  three-day cooldown, and a pre-commit configuration.
@@ -101,9 +176,11 @@ First release.
101
176
  `alpha`, plus p95-latency and cost budgets, with distinct exit codes.
102
177
  - Content-addressed response cache; runs record the Ollama version and model digests.
103
178
  - Markdown reports for GitHub step summaries, JSON reports, and a composite GitHub Action.
104
- - Two examples: an offline ticket router (CI's gate contract) and a support assistant for
105
- a fictional product judged by a local model, with measurements.
179
+ - Two examples: an offline ticket router (CI's gate contract) and a product support
180
+ assistant judged by a local model, with measurements.
106
181
 
182
+ [Unreleased]: https://github.com/technobasant/evalproof/compare/v0.3.0...HEAD
183
+ [0.3.0]: https://github.com/technobasant/evalproof/compare/v0.2.1...v0.3.0
107
184
  [0.2.1]: https://github.com/technobasant/evalproof/compare/v0.2.0...v0.2.1
108
185
  [0.2.0]: https://github.com/technobasant/evalproof/compare/v0.1.2...v0.2.0
109
186
  [0.1.2]: https://github.com/technobasant/evalproof/compare/v0.1.1...v0.1.2
@@ -11,8 +11,8 @@ authors:
11
11
  - family-names: Bhattarai
12
12
  given-names: Basant
13
13
  website: "https://basantbhattarai.com.np"
14
- version: 0.2.1
15
- date-released: "2026-10-16"
14
+ version: 0.3.0
15
+ date-released: "2026-11-12"
16
16
  license: Apache-2.0
17
17
  repository-code: "https://github.com/technobasant/evalproof"
18
18
  url: "https://pypi.org/project/evalproof/"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: evalproof
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Treat LLM evaluation as measurement: calibrate the judge, correct the pass rate, gate CI on significant regressions.
5
5
  Project-URL: Homepage, https://github.com/technobasant/evalproof
6
6
  Project-URL: Documentation, https://github.com/technobasant/evalproof#readme
@@ -67,8 +67,9 @@ evalproof:
67
67
  4. **gates CI** only when a drop is both larger than your tolerance *and* statistically
68
68
  significant, plus latency and cost budgets, with distinct exit codes.
69
69
 
70
- It is local-first: Ollama is the default provider, Anthropic and OpenAI are optional, and
71
- every model call is cached by content so re-runs are free and reproducible.
70
+ It is local-first: Ollama is the default, every major hosted provider is one `target:` line
71
+ away ([Providers](#providers)), and every model call is cached by content so re-runs are free
72
+ and reproducible.
72
73
 
73
74
  ## Install
74
75
 
@@ -78,12 +79,12 @@ uv tool install evalproof # just the CLI
78
79
  ```
79
80
 
80
81
  Python 3.11 to 3.14. `compare`, `calibrate` and `report` work on run files and need no
81
- model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local
82
- Ollama model, or an Anthropic or OpenAI key.
82
+ model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local model
83
+ (Ollama, vLLM, LM Studio, llama.cpp), or a key for a hosted provider.
83
84
 
84
85
  ## What it found on a real run
85
86
 
86
- A local `qwen3:1.7b` support assistant for a fictional product, judged by `qwen3:8b`, 83
87
+ A local `qwen3:1.7b` product support assistant, judged by `qwen3:8b`, 83
87
88
  questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md).
88
89
 
89
90
  | Question | Measured |
@@ -94,7 +95,8 @@ questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](https:
94
95
  | Does correcting the judge's rate help? | On average yes (offset 4.8 → ~3.8 pts), but with 30 labels the interval doubles; at 83 cases the judge's bias is smaller than its own noise |
95
96
  | Was the gold trustworthy? | No: of 13 judge–key disagreements the key was wrong 7 times, the judge 4. Most of the "bias" corrected was the gold's |
96
97
  | Is pairwise judging safer? | No: 42% of verdicts flipped with the order of the two answers, and the judge preferred the candidate 42 to 6 — mostly where both answers were right and the candidate's was five times longer |
97
- | Is it reproducible? | 20/20 identical outputs and verdicts with the cache off; a warm-cache re-run took 0.05 s instead of 363 s |
98
+ | Does a reasoning judge help? | Against audited labels, yes: thinking on passed no wrong answer (three without it) and κ rose from 0.86–0.88 to 0.90–0.94, at 6.7× the judge time. Against the raw answer key it looked worse, because the key is wrong on exactly those cases. The gate's decision did not change |
99
+ | Is it reproducible? | 20/20 identical outputs and verdicts with the cache off; a warm-cache re-run took 0.05 s instead of 363 s; a re-judge three days later matched 83/83 |
98
100
 
99
101
  ## Five-minute quickstart
100
102
 
@@ -115,6 +117,11 @@ uv run evalproof compare runs/baseline.json runs/noisy.json \
115
117
  --suite examples/deterministic/suite.yaml --gate; echo "exit $?" # 0: 2 of 60 is noise
116
118
  ```
117
119
 
120
+ [`examples/retrieval`](https://github.com/technobasant/evalproof/blob/main/examples/retrieval) runs the same gate on a retrieval change: recall@5
121
+ does not move when the top hit drops to rank 5, while MRR and nDCG@5 do, and the gate fails
122
+ it. It was contributed by [@HarshRajSinghania](https://github.com/HarshRajSinghania), and CI
123
+ checks its exit codes too.
124
+
118
125
  With Ollama installed, the LLM example is
119
126
  [`examples/support-qa`](https://github.com/technobasant/evalproof/blob/main/examples/support-qa) (`ollama pull qwen3:1.7b` and
120
127
  `ollama pull qwen3:8b`; about 6–10 minutes per run on an Apple M5).
@@ -122,6 +129,63 @@ With Ollama installed, the LLM example is
122
129
  The browser app in [`app/`](https://github.com/technobasant/evalproof/blob/main/app) runs locally too:
123
130
  `uv run --with-requirements app/requirements.txt streamlit run app/streamlit_app.py`.
124
131
 
132
+ ## Providers
133
+
134
+ The target is `<provider>:<model>`; the model name is everything after the first colon, so
135
+ `openrouter:anthropic/claude-sonnet-4.5` and `ollama:qwen3:8b` both work. Two native adapters
136
+ (`ollama`, `anthropic`) and one OpenAI-compatible adapter driven by a table of provider
137
+ profiles ([`providers/profiles.py`](https://github.com/technobasant/evalproof/blob/main/src/evalproof/providers/profiles.py), each row cites the
138
+ provider's documentation). Plain HTTP throughout, no vendor SDK.
139
+
140
+ | Provider | Example target | Key variable | Where |
141
+ |---|---|---|---|
142
+ | `ollama` | `ollama:qwen3:8b` | none | local |
143
+ | `vllm` | `vllm:meta-llama/Llama-3.1-8B-Instruct` | `VLLM_API_KEY`, optional | local |
144
+ | `lmstudio` | `lmstudio:qwen3-8b` | optional, via `api_key_env` | local |
145
+ | `llamacpp` | `llamacpp:qwen3-8b` | `LLAMA_API_KEY`, optional | local |
146
+ | `anthropic` | `anthropic:claude-sonnet-4-5` | `ANTHROPIC_API_KEY` | hosted |
147
+ | `openai` | `openai:gpt-5` | `OPENAI_API_KEY` | hosted |
148
+ | `azure` | `azure:<deployment>` | `AZURE_OPENAI_API_KEY` | hosted, needs `base_url` |
149
+ | `gemini` | `gemini:gemini-2.5-flash` | `GEMINI_API_KEY` | hosted |
150
+ | `mistral` | `mistral:mistral-large-latest` | `MISTRAL_API_KEY` | hosted |
151
+ | `groq` | `groq:llama-3.3-70b-versatile` | `GROQ_API_KEY` | hosted |
152
+ | `together` | `together:meta-llama/Llama-3.3-70B-Instruct-Turbo` | `TOGETHER_API_KEY` | hosted |
153
+ | `fireworks` | `fireworks:accounts/fireworks/models/llama-v3p3-70b-instruct` | `FIREWORKS_API_KEY` | hosted |
154
+ | `deepseek` | `deepseek:deepseek-chat` | `DEEPSEEK_API_KEY` | hosted |
155
+ | `xai` | `xai:grok-4` | `XAI_API_KEY` | hosted |
156
+ | `openrouter` | `openrouter:anthropic/claude-sonnet-4.5` | `OPENROUTER_API_KEY` | hosted |
157
+ | `cerebras` | `cerebras:gpt-oss-120b` | `CEREBRAS_API_KEY` | hosted |
158
+ | `openai-compatible` | `openai-compatible:<model>` | optional, via `api_key_env` | any server, needs `base_url` |
159
+
160
+ ```yaml
161
+ system:
162
+ target: openai-compatible:gpt-4o # a gateway, LiteLLM proxy, TGI, SGLang, ...
163
+ base_url: https://llm-gateway.internal/v1
164
+ api_key_env: TEAM_GATEWAY_KEY # the variable to read; the key itself never goes in the suite
165
+ headers: {x-team: search} # extra HTTP headers on every call, never secrets
166
+ params: {max_tokens: 2048}
167
+ ```
168
+
169
+ * `base_url` overrides any profile's default (a regional endpoint, a proxy, an older
170
+ `llama-server`, which listened on port 8080 before October 2026 and now defaults to 9931).
171
+ * `api_key_env` replaces the profile's variable for any provider; a variable named there
172
+ must be set.
173
+ * `headers` merge into every request and win over the adapter's own, for example
174
+ `anthropic-version`. They are not part of the cache key, and a suite file is usually
175
+ committed, so keep keys in the environment.
176
+ * Sampling: local servers (`ollama`, `vllm`, `lmstudio`, `llamacpp`) receive `temperature` and
177
+ `seed` on every call, defaults included, so a local run repeats. Hosted providers receive
178
+ them only when `params` sets them, because reasoning models reject a non-default
179
+ temperature; Anthropic never receives a seed, and since its current models also reject
180
+ `temperature`, it gets one only when the suite asks. `params.extra` adds any other body
181
+ field, such as `{thinking: {type: adaptive}, output_config: {effort: low}}`.
182
+ * Thinking models spend output tokens on thinking inside `max_tokens`, which defaults to 512
183
+ (changing it would change every cache key). Set `params.max_tokens` higher for them; a reply
184
+ cut off before its first word, a refusal, or a content-filter block is recorded as that
185
+ case's error rather than as an empty answer.
186
+ * AWS Bedrock and Google Vertex AI sign requests with cloud credentials, which evalproof does
187
+ not implement. Use a gateway's OpenAI-compatible endpoint through `openai-compatible`.
188
+
125
189
  ## Concepts
126
190
 
127
191
  **Suite** — one YAML file: a JSONL golden set, the system under test, scorers, an optional
@@ -131,7 +195,7 @@ judge, calibration settings and a gate.
131
195
  name: support-qa
132
196
  dataset: cases.jsonl # {"id", "input", "reference", ...extra fields}
133
197
  system:
134
- target: ollama:qwen3:1.7b # or anthropic:…, openai:…, python:module:func, https://…
198
+ target: ollama:qwen3:1.7b # or groq:…, anthropic:…, python:module:func, https://…
135
199
  system_prompt_file: prompt.md
136
200
  prompt_template: "{question}"
137
201
  params: {temperature: 0, seed: 0, think: false}
@@ -174,6 +238,56 @@ orders and reports order-dependent verdicts as ties.
174
238
  | 4 | cannot gate: fewer than `min_cases` paired cases, or the runs used different data |
175
239
  | 2 | usage or configuration error |
176
240
 
241
+ The gate checks one metric by default. To gate several, repeat `--metric` or list them in
242
+ the suite; each metric's p-value is then Holm-adjusted for the number of metrics gated, so
243
+ the chance of failing a build on noise stays at `alpha` for the set as a whole, and the
244
+ report shows the raw and the adjusted p-value side by side. Metrics that are not gated are
245
+ still reported.
246
+
247
+ ```yaml
248
+ gate:
249
+ metrics: [judge, answer_key] # one metric: `metric: judge`
250
+ max_drop: 0.03
251
+ alpha: 0.05
252
+ min_cases: 40
253
+ ```
254
+
255
+ ```bash
256
+ evalproof compare baseline.json candidate.json --gate --metric judge --metric answer_key
257
+ ```
258
+
259
+ ## Import results from other tools
260
+
261
+ You do not have to run your evals with evalproof to gate them. `evalproof import` turns the
262
+ per-case results another tool wrote into a run file that `compare` and the gate read:
263
+
264
+ ```bash
265
+ evalproof import promptfoo output.json --system openai:gpt-5 --out runs/candidate.json
266
+ evalproof import inspect logs/task.eval --out runs/candidate.json
267
+ evalproof import deepeval .deepeval/.latest_test_run.json --out runs/candidate.json
268
+ evalproof import lm-eval results/gpt2/samples_hellaswag_2026-11-12T10-20-30.jsonl --out runs/gpt2.json
269
+ evalproof import csv results.csv --system a --out runs/a.json # id,metric,passed,score,...
270
+ evalproof import jsonl results.jsonl --wide --id-field case --out runs/a.json # one column per metric
271
+ ```
272
+
273
+ Import each side with the same importer, then gate as usual, for example
274
+ `evalproof compare runs/baseline.json runs/candidate.json --gate --metric pass`. What to know:
275
+
276
+ * Case ids are stable between runs of the same dataset (they come from the test's inputs, or
277
+ the tool's own sample id, never from its position), which is what lets two files pair.
278
+ The dataset hash covers the ids and inputs as the tool recorded them, so runs imported by
279
+ different importers refuse to gate (exit 4) instead of pairing on a coincidence.
280
+ * A system error counts as a failure on every metric, as in a native run. Repeats of one case
281
+ (epochs, repeated tests) collapse by `--epochs`: `mean` (the default), `first`, `all`
282
+ or `any`. Under `mean`, `first` and `any`, a case with at least one repeat that ran is not
283
+ counted as a system error; its errored repeats still score as failures.
284
+ * A NaN or infinite score is refused at import, naming the case and metric, because it would
285
+ make the bootstrap report a significant difference that is not there.
286
+ * promptfoo files can hold several providers and `--system` picks one. Inspect `.eval` logs
287
+ compressed with zstd need Python 3.14 or newer; otherwise run
288
+ `inspect log convert --to json <file>` first. DeepEval's conversational cases are skipped
289
+ with a warning.
290
+
177
291
  ## From Python
178
292
 
179
293
  The CLI is a thin layer over the same functions:
@@ -207,7 +321,7 @@ The architecture diagram is in the [README on GitHub](https://github.com/technob
207
321
  | Treat the judge as a classifier to be calibrated | Trust the judge's pass rate | Its count is biased by its own error rates, and the bias moves with the true rate | [0001](https://github.com/technobasant/evalproof/blob/main/docs/adr/0001-judge-is-a-classifier.md) |
208
322
  | Local Ollama by default, HTTP-only providers, content-addressed cache | Hosted APIs and vendor SDKs | Reproducible, free to re-run, tied to a model digest; no SDK dependency | [0002](https://github.com/technobasant/evalproof/blob/main/docs/adr/0002-local-first-providers.md) |
209
323
  | Gate on "large AND significant", on paired cases | "Fail if the score dropped by X" | On a few dozen cases, X-point drops are mostly noise, and a noisy gate gets switched off | [0003](https://github.com/technobasant/evalproof/blob/main/docs/adr/0003-gate-on-significance-not-delta.md) |
210
- | Gate one metric; report the rest | Gate every metric | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
324
+ | One metric by default; several with Holm correction | Gate every metric at α each | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
211
325
  | Pointwise verdicts for gating; pairwise as a diagnostic | Pairwise preference for gating | Measured: 42% of pairwise verdicts flipped with answer order, with a strong length preference | [MEASUREMENTS §6](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md) |
212
326
  | numpy and the standard library for statistics | SciPy, statsmodels | Small enough to read and to test against hand-worked values | — |
213
327
 
@@ -217,11 +331,12 @@ The composite action runs `compare --gate`, appends the report to the job summar
217
331
  fails the job on exit codes 1, 3 or 4:
218
332
 
219
333
  ```yaml
220
- - uses: technobasant/evalproof@v0.2.0
334
+ - uses: technobasant/evalproof@v0.3.0
221
335
  with:
222
336
  baseline: eval/baseline.json # produced on main, e.g. as a build artifact
223
337
  candidate: eval/candidate.json # produced on this branch
224
338
  suite: eval/suite.yaml
339
+ # metric: "judge, answer_key" # optional: gate several metrics, newline- or comma-separated
225
340
  ```
226
341
 
227
342
  This repository's own CI runs the offline example on every commit and asserts that the
@@ -241,11 +356,12 @@ frameworks.
241
356
  evalproof is deliberately narrow: it is the measurement layer — how much a judge's verdict
242
357
  is worth, and whether a difference between two runs is real. Its run file is plain,
243
358
  documented JSON, so it can sit beside an existing harness: produce the per-case verdicts
244
- however you like, then calibrate and gate here.
359
+ however you like, then calibrate and gate here. For promptfoo, Inspect, DeepEval and
360
+ lm-eval you do not even need to do that: evalproof [imports their results](#import-results-from-other-tools).
245
361
 
246
362
  ## Limits
247
363
 
248
- * The measurements cover one machine, two small local models, one fictional domain and
364
+ * The measurements cover one machine, two small local models, one product domain and
249
365
  83 cases. They show the method; they are not a benchmark of those models.
250
366
  * Calibration is only as good as its gold labels — [MEASUREMENTS §4](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md)
251
367
  shows a regex answer key being wrong more often than the judge. Use human labels when
@@ -260,9 +376,6 @@ however you like, then calibrate and gate here.
260
376
  ## Roadmap
261
377
 
262
378
  * Stratified gold sampling (by tag) and per-tag calibration.
263
- * Multiple-comparison correction when more than one metric is gated.
264
- * A reasoning judge (`think: true`) measured against the same answer key.
265
- * Importers for other harnesses' per-case results.
266
379
 
267
380
  ## Project
268
381
 
@@ -31,8 +31,9 @@ evalproof:
31
31
  4. **gates CI** only when a drop is both larger than your tolerance *and* statistically
32
32
  significant, plus latency and cost budgets, with distinct exit codes.
33
33
 
34
- It is local-first: Ollama is the default provider, Anthropic and OpenAI are optional, and
35
- every model call is cached by content so re-runs are free and reproducible.
34
+ It is local-first: Ollama is the default, every major hosted provider is one `target:` line
35
+ away ([Providers](#providers)), and every model call is cached by content so re-runs are free
36
+ and reproducible.
36
37
 
37
38
  ## Install
38
39
 
@@ -42,12 +43,12 @@ uv tool install evalproof # just the CLI
42
43
  ```
43
44
 
44
45
  Python 3.11 to 3.14. `compare`, `calibrate` and `report` work on run files and need no
45
- model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local
46
- Ollama model, or an Anthropic or OpenAI key.
46
+ model. `run` needs a system to evaluate: a Python function, an HTTP endpoint, a local model
47
+ (Ollama, vLLM, LM Studio, llama.cpp), or a key for a hosted provider.
47
48
 
48
49
  ## What it found on a real run
49
50
 
50
- A local `qwen3:1.7b` support assistant for a fictional product, judged by `qwen3:8b`, 83
51
+ A local `qwen3:1.7b` product support assistant, judged by `qwen3:8b`, 83
51
52
  questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](MEASUREMENTS.md).
52
53
 
53
54
  | Question | Measured |
@@ -58,7 +59,8 @@ questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](MEASUR
58
59
  | Does correcting the judge's rate help? | On average yes (offset 4.8 → ~3.8 pts), but with 30 labels the interval doubles; at 83 cases the judge's bias is smaller than its own noise |
59
60
  | Was the gold trustworthy? | No: of 13 judge–key disagreements the key was wrong 7 times, the judge 4. Most of the "bias" corrected was the gold's |
60
61
  | Is pairwise judging safer? | No: 42% of verdicts flipped with the order of the two answers, and the judge preferred the candidate 42 to 6 — mostly where both answers were right and the candidate's was five times longer |
61
- | Is it reproducible? | 20/20 identical outputs and verdicts with the cache off; a warm-cache re-run took 0.05 s instead of 363 s |
62
+ | Does a reasoning judge help? | Against audited labels, yes: thinking on passed no wrong answer (three without it) and κ rose from 0.86–0.88 to 0.90–0.94, at 6.7× the judge time. Against the raw answer key it looked worse, because the key is wrong on exactly those cases. The gate's decision did not change |
63
+ | Is it reproducible? | 20/20 identical outputs and verdicts with the cache off; a warm-cache re-run took 0.05 s instead of 363 s; a re-judge three days later matched 83/83 |
62
64
 
63
65
  ## Five-minute quickstart
64
66
 
@@ -79,6 +81,11 @@ uv run evalproof compare runs/baseline.json runs/noisy.json \
79
81
  --suite examples/deterministic/suite.yaml --gate; echo "exit $?" # 0: 2 of 60 is noise
80
82
  ```
81
83
 
84
+ [`examples/retrieval`](examples/retrieval) runs the same gate on a retrieval change: recall@5
85
+ does not move when the top hit drops to rank 5, while MRR and nDCG@5 do, and the gate fails
86
+ it. It was contributed by [@HarshRajSinghania](https://github.com/HarshRajSinghania), and CI
87
+ checks its exit codes too.
88
+
82
89
  With Ollama installed, the LLM example is
83
90
  [`examples/support-qa`](examples/support-qa) (`ollama pull qwen3:1.7b` and
84
91
  `ollama pull qwen3:8b`; about 6–10 minutes per run on an Apple M5).
@@ -86,6 +93,63 @@ With Ollama installed, the LLM example is
86
93
  The browser app in [`app/`](app) runs locally too:
87
94
  `uv run --with-requirements app/requirements.txt streamlit run app/streamlit_app.py`.
88
95
 
96
+ ## Providers
97
+
98
+ The target is `<provider>:<model>`; the model name is everything after the first colon, so
99
+ `openrouter:anthropic/claude-sonnet-4.5` and `ollama:qwen3:8b` both work. Two native adapters
100
+ (`ollama`, `anthropic`) and one OpenAI-compatible adapter driven by a table of provider
101
+ profiles ([`providers/profiles.py`](src/evalproof/providers/profiles.py), each row cites the
102
+ provider's documentation). Plain HTTP throughout, no vendor SDK.
103
+
104
+ | Provider | Example target | Key variable | Where |
105
+ |---|---|---|---|
106
+ | `ollama` | `ollama:qwen3:8b` | none | local |
107
+ | `vllm` | `vllm:meta-llama/Llama-3.1-8B-Instruct` | `VLLM_API_KEY`, optional | local |
108
+ | `lmstudio` | `lmstudio:qwen3-8b` | optional, via `api_key_env` | local |
109
+ | `llamacpp` | `llamacpp:qwen3-8b` | `LLAMA_API_KEY`, optional | local |
110
+ | `anthropic` | `anthropic:claude-sonnet-4-5` | `ANTHROPIC_API_KEY` | hosted |
111
+ | `openai` | `openai:gpt-5` | `OPENAI_API_KEY` | hosted |
112
+ | `azure` | `azure:<deployment>` | `AZURE_OPENAI_API_KEY` | hosted, needs `base_url` |
113
+ | `gemini` | `gemini:gemini-2.5-flash` | `GEMINI_API_KEY` | hosted |
114
+ | `mistral` | `mistral:mistral-large-latest` | `MISTRAL_API_KEY` | hosted |
115
+ | `groq` | `groq:llama-3.3-70b-versatile` | `GROQ_API_KEY` | hosted |
116
+ | `together` | `together:meta-llama/Llama-3.3-70B-Instruct-Turbo` | `TOGETHER_API_KEY` | hosted |
117
+ | `fireworks` | `fireworks:accounts/fireworks/models/llama-v3p3-70b-instruct` | `FIREWORKS_API_KEY` | hosted |
118
+ | `deepseek` | `deepseek:deepseek-chat` | `DEEPSEEK_API_KEY` | hosted |
119
+ | `xai` | `xai:grok-4` | `XAI_API_KEY` | hosted |
120
+ | `openrouter` | `openrouter:anthropic/claude-sonnet-4.5` | `OPENROUTER_API_KEY` | hosted |
121
+ | `cerebras` | `cerebras:gpt-oss-120b` | `CEREBRAS_API_KEY` | hosted |
122
+ | `openai-compatible` | `openai-compatible:<model>` | optional, via `api_key_env` | any server, needs `base_url` |
123
+
124
+ ```yaml
125
+ system:
126
+ target: openai-compatible:gpt-4o # a gateway, LiteLLM proxy, TGI, SGLang, ...
127
+ base_url: https://llm-gateway.internal/v1
128
+ api_key_env: TEAM_GATEWAY_KEY # the variable to read; the key itself never goes in the suite
129
+ headers: {x-team: search} # extra HTTP headers on every call, never secrets
130
+ params: {max_tokens: 2048}
131
+ ```
132
+
133
+ * `base_url` overrides any profile's default (a regional endpoint, a proxy, an older
134
+ `llama-server`, which listened on port 8080 before October 2026 and now defaults to 9931).
135
+ * `api_key_env` replaces the profile's variable for any provider; a variable named there
136
+ must be set.
137
+ * `headers` merge into every request and win over the adapter's own, for example
138
+ `anthropic-version`. They are not part of the cache key, and a suite file is usually
139
+ committed, so keep keys in the environment.
140
+ * Sampling: local servers (`ollama`, `vllm`, `lmstudio`, `llamacpp`) receive `temperature` and
141
+ `seed` on every call, defaults included, so a local run repeats. Hosted providers receive
142
+ them only when `params` sets them, because reasoning models reject a non-default
143
+ temperature; Anthropic never receives a seed, and since its current models also reject
144
+ `temperature`, it gets one only when the suite asks. `params.extra` adds any other body
145
+ field, such as `{thinking: {type: adaptive}, output_config: {effort: low}}`.
146
+ * Thinking models spend output tokens on thinking inside `max_tokens`, which defaults to 512
147
+ (changing it would change every cache key). Set `params.max_tokens` higher for them; a reply
148
+ cut off before its first word, a refusal, or a content-filter block is recorded as that
149
+ case's error rather than as an empty answer.
150
+ * AWS Bedrock and Google Vertex AI sign requests with cloud credentials, which evalproof does
151
+ not implement. Use a gateway's OpenAI-compatible endpoint through `openai-compatible`.
152
+
89
153
  ## Concepts
90
154
 
91
155
  **Suite** — one YAML file: a JSONL golden set, the system under test, scorers, an optional
@@ -95,7 +159,7 @@ judge, calibration settings and a gate.
95
159
  name: support-qa
96
160
  dataset: cases.jsonl # {"id", "input", "reference", ...extra fields}
97
161
  system:
98
- target: ollama:qwen3:1.7b # or anthropic:…, openai:…, python:module:func, https://…
162
+ target: ollama:qwen3:1.7b # or groq:…, anthropic:…, python:module:func, https://…
99
163
  system_prompt_file: prompt.md
100
164
  prompt_template: "{question}"
101
165
  params: {temperature: 0, seed: 0, think: false}
@@ -138,6 +202,56 @@ orders and reports order-dependent verdicts as ties.
138
202
  | 4 | cannot gate: fewer than `min_cases` paired cases, or the runs used different data |
139
203
  | 2 | usage or configuration error |
140
204
 
205
+ The gate checks one metric by default. To gate several, repeat `--metric` or list them in
206
+ the suite; each metric's p-value is then Holm-adjusted for the number of metrics gated, so
207
+ the chance of failing a build on noise stays at `alpha` for the set as a whole, and the
208
+ report shows the raw and the adjusted p-value side by side. Metrics that are not gated are
209
+ still reported.
210
+
211
+ ```yaml
212
+ gate:
213
+ metrics: [judge, answer_key] # one metric: `metric: judge`
214
+ max_drop: 0.03
215
+ alpha: 0.05
216
+ min_cases: 40
217
+ ```
218
+
219
+ ```bash
220
+ evalproof compare baseline.json candidate.json --gate --metric judge --metric answer_key
221
+ ```
222
+
223
+ ## Import results from other tools
224
+
225
+ You do not have to run your evals with evalproof to gate them. `evalproof import` turns the
226
+ per-case results another tool wrote into a run file that `compare` and the gate read:
227
+
228
+ ```bash
229
+ evalproof import promptfoo output.json --system openai:gpt-5 --out runs/candidate.json
230
+ evalproof import inspect logs/task.eval --out runs/candidate.json
231
+ evalproof import deepeval .deepeval/.latest_test_run.json --out runs/candidate.json
232
+ evalproof import lm-eval results/gpt2/samples_hellaswag_2026-11-12T10-20-30.jsonl --out runs/gpt2.json
233
+ evalproof import csv results.csv --system a --out runs/a.json # id,metric,passed,score,...
234
+ evalproof import jsonl results.jsonl --wide --id-field case --out runs/a.json # one column per metric
235
+ ```
236
+
237
+ Import each side with the same importer, then gate as usual, for example
238
+ `evalproof compare runs/baseline.json runs/candidate.json --gate --metric pass`. What to know:
239
+
240
+ * Case ids are stable between runs of the same dataset (they come from the test's inputs, or
241
+ the tool's own sample id, never from its position), which is what lets two files pair.
242
+ The dataset hash covers the ids and inputs as the tool recorded them, so runs imported by
243
+ different importers refuse to gate (exit 4) instead of pairing on a coincidence.
244
+ * A system error counts as a failure on every metric, as in a native run. Repeats of one case
245
+ (epochs, repeated tests) collapse by `--epochs`: `mean` (the default), `first`, `all`
246
+ or `any`. Under `mean`, `first` and `any`, a case with at least one repeat that ran is not
247
+ counted as a system error; its errored repeats still score as failures.
248
+ * A NaN or infinite score is refused at import, naming the case and metric, because it would
249
+ make the bootstrap report a significant difference that is not there.
250
+ * promptfoo files can hold several providers and `--system` picks one. Inspect `.eval` logs
251
+ compressed with zstd need Python 3.14 or newer; otherwise run
252
+ `inspect log convert --to json <file>` first. DeepEval's conversational cases are skipped
253
+ with a warning.
254
+
141
255
  ## From Python
142
256
 
143
257
  The CLI is a thin layer over the same functions:
@@ -167,7 +281,7 @@ flowchart LR
167
281
  suite[suite.yaml] --> runner
168
282
  data[(cases.jsonl)] --> runner
169
283
  subgraph runner[evalproof run]
170
- sys[system under test<br/>ollama · anthropic · openai<br/>python · http] --> scorers
284
+ sys[system under test<br/>ollama · anthropic · openai-compatible<br/>python · http] --> scorers
171
285
  scorers[scorers<br/>deterministic · retrieval · llm_judge]
172
286
  cache[(content-addressed<br/>cache)]
173
287
  sys <--> cache
@@ -189,7 +303,7 @@ flowchart LR
189
303
  | Treat the judge as a classifier to be calibrated | Trust the judge's pass rate | Its count is biased by its own error rates, and the bias moves with the true rate | [0001](docs/adr/0001-judge-is-a-classifier.md) |
190
304
  | Local Ollama by default, HTTP-only providers, content-addressed cache | Hosted APIs and vendor SDKs | Reproducible, free to re-run, tied to a model digest; no SDK dependency | [0002](docs/adr/0002-local-first-providers.md) |
191
305
  | Gate on "large AND significant", on paired cases | "Fail if the score dropped by X" | On a few dozen cases, X-point drops are mostly noise, and a noisy gate gets switched off | [0003](docs/adr/0003-gate-on-significance-not-delta.md) |
192
- | Gate one metric; report the rest | Gate every metric | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
306
+ | One metric by default; several with Holm correction | Gate every metric at α each | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
193
307
  | Pointwise verdicts for gating; pairwise as a diagnostic | Pairwise preference for gating | Measured: 42% of pairwise verdicts flipped with answer order, with a strong length preference | [MEASUREMENTS §6](MEASUREMENTS.md) |
194
308
  | numpy and the standard library for statistics | SciPy, statsmodels | Small enough to read and to test against hand-worked values | — |
195
309
 
@@ -199,11 +313,12 @@ The composite action runs `compare --gate`, appends the report to the job summar
199
313
  fails the job on exit codes 1, 3 or 4:
200
314
 
201
315
  ```yaml
202
- - uses: technobasant/evalproof@v0.2.0
316
+ - uses: technobasant/evalproof@v0.3.0
203
317
  with:
204
318
  baseline: eval/baseline.json # produced on main, e.g. as a build artifact
205
319
  candidate: eval/candidate.json # produced on this branch
206
320
  suite: eval/suite.yaml
321
+ # metric: "judge, answer_key" # optional: gate several metrics, newline- or comma-separated
207
322
  ```
208
323
 
209
324
  This repository's own CI runs the offline example on every commit and asserts that the
@@ -223,11 +338,12 @@ frameworks.
223
338
  evalproof is deliberately narrow: it is the measurement layer — how much a judge's verdict
224
339
  is worth, and whether a difference between two runs is real. Its run file is plain,
225
340
  documented JSON, so it can sit beside an existing harness: produce the per-case verdicts
226
- however you like, then calibrate and gate here.
341
+ however you like, then calibrate and gate here. For promptfoo, Inspect, DeepEval and
342
+ lm-eval you do not even need to do that: evalproof [imports their results](#import-results-from-other-tools).
227
343
 
228
344
  ## Limits
229
345
 
230
- * The measurements cover one machine, two small local models, one fictional domain and
346
+ * The measurements cover one machine, two small local models, one product domain and
231
347
  83 cases. They show the method; they are not a benchmark of those models.
232
348
  * Calibration is only as good as its gold labels — [MEASUREMENTS §4](MEASUREMENTS.md)
233
349
  shows a regex answer key being wrong more often than the judge. Use human labels when
@@ -242,9 +358,6 @@ however you like, then calibrate and gate here.
242
358
  ## Roadmap
243
359
 
244
360
  * Stratified gold sampling (by tag) and per-tag calibration.
245
- * Multiple-comparison correction when more than one metric is gated.
246
- * A reasoning judge (`think: true`) measured against the same answer key.
247
- * Importers for other harnesses' per-case results.
248
361
 
249
362
  ## Project
250
363
 
@@ -23,10 +23,11 @@ from evalproof.gate import (
23
23
  GateDecision,
24
24
  decide,
25
25
  )
26
+ from evalproof.importers import ImportFormatError, import_run
26
27
  from evalproof.models import Case, CaseResult, Run, RunInfo, Score, SystemOutput
27
28
  from evalproof.report import calibration_markdown, comparison_markdown, run_markdown
28
29
  from evalproof.runner import load_run, run_suite, save_run
29
- from evalproof.stats import mcnemar_exact, paired_bootstrap, wilson
30
+ from evalproof.stats import holm, mcnemar_exact, paired_bootstrap, wilson
30
31
 
31
32
  __all__ = [
32
33
  "EXIT_BUDGET",
@@ -44,6 +45,7 @@ __all__ = [
44
45
  "Estimate",
45
46
  "GateConfig",
46
47
  "GateDecision",
48
+ "ImportFormatError",
47
49
  "MetricComparison",
48
50
  "Run",
49
51
  "RunInfo",
@@ -57,6 +59,8 @@ __all__ = [
57
59
  "compare_runs",
58
60
  "comparison_markdown",
59
61
  "decide",
62
+ "holm",
63
+ "import_run",
60
64
  "load_config",
61
65
  "load_dataset",
62
66
  "load_run",
@@ -1,3 +1,3 @@
1
1
  """The package version: the single place it is written. pyproject.toml reads it at build time."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.3.0"