evalix 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.
@@ -0,0 +1,7 @@
1
+ runs/
2
+ __pycache__/
3
+ .env
4
+ .venv/
5
+ dist/
6
+ *.egg-info/
7
+ .pytest_cache/
evalix-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Sawicki
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.
evalix-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,292 @@
1
+ Metadata-Version: 2.5
2
+ Name: evalix
3
+ Version: 0.1.0
4
+ Summary: A small eval harness that tells you which cases your prompt change broke.
5
+ Project-URL: Homepage, https://github.com/frycz/evalix
6
+ Project-URL: Repository, https://github.com/frycz/evalix
7
+ Project-URL: Issues, https://github.com/frycz/evalix/issues
8
+ Author: Adam Sawicki
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: benchmark,eval,evaluation,llm,prompt-engineering
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: capix>=0.1.2
25
+ Requires-Dist: python-dotenv>=1.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # evalix
29
+
30
+ A small eval harness that tells you **which cases your prompt change broke**.
31
+
32
+ ```
33
+ score 0.136 → 0.136 (+0.000) over 22 shared cases
34
+ fixed t11
35
+ BROKE t14
36
+ ```
37
+
38
+ A flat score. One case fixed, one case broken. Without the second line you would
39
+ have called that "no change" and moved on.
40
+
41
+ That is the whole pitch. The score tells you *whether*; the diff tells you
42
+ *which*, and only the second one teaches you anything.
43
+
44
+ ## The loop
45
+
46
+ > fixed case set → change **one** thing → measure → keep or revert
47
+
48
+ The usual way people practise prompt engineering is to tweak a prompt, look at
49
+ one output, decide it's better, and move on. That's not a skill, it's a feeling.
50
+ Everything here is built around the loop above.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install evalix # or: uv add evalix
56
+ ```
57
+
58
+ The default runner talks to Claude through [capix](https://pypi.org/project/capix/),
59
+ so `ANTHROPIC_API_KEY` in your environment or a `.env` file is all the setup
60
+ there is. Any other provider is a three-line function — see [Runners](#runners).
61
+
62
+ ## Use it
63
+
64
+ A case file is JSONL, one case per line:
65
+
66
+ ```json
67
+ {"id": "t01", "tag": "easy", "input": "I was charged twice.", "expected": "billing"}
68
+ {"id": "t02", "tag": "edge", "input": "Export CSV throws a TypeError.", "expected": "bug"}
69
+ ```
70
+
71
+ ```bash
72
+ evalix run --cases cases.jsonl --prompt prompts/v1.txt --scorer exact
73
+ ```
74
+
75
+ ```
76
+ ✓ t01 1.00 want='billing' got='billing'
77
+ ✗ t02 0.00 want='bug' got='how_to'
78
+
79
+ score 0.727 (16/22 perfect)
80
+ tokens 1534 in / 212 out ≈ $0.0026
81
+
82
+ 0.636 → 0.727 (+0.091) over 22 shared cases
83
+ fixed t09, t11, t14
84
+ BROKE t20
85
+
86
+ worst 5:
87
+ t20 (0.00) want='how_to' got='feature_request'
88
+ → feature_request
89
+ ```
90
+
91
+ Runs are saved to `runs/` and the next run of the same case file is diffed
92
+ against them automatically. To compare any two runs — v1 against v5, with three
93
+ experiments in between:
94
+
95
+ ```bash
96
+ evalix compare v1-lazy v5-spec
97
+ ```
98
+
99
+ Each argument is a run file or any substring of one. Reruns of the same
100
+ experiment resolve to the newest; a substring that matches different runs (`v1`
101
+ also matching `v10`) is an error rather than a guess. It prints the metadata side
102
+ by side, flags every field that differs, warns when more than one axis moved at
103
+ once, then gives the per-case changes, a per-tag breakdown, and the outputs
104
+ behind each regression.
105
+
106
+ ## Spend nothing while you work
107
+
108
+ `--dry-run` sends nothing. It resolves the case file, the prompt and the scorer,
109
+ prints what each call would contain, and estimates the bill:
110
+
111
+ ```bash
112
+ evalix run --cases cases.jsonl --prompt prompts/v1.txt --dry-run
113
+ ```
114
+
115
+ ```
116
+ 22 calls · ~3211 input tokens
117
+ worst case 44000 output tokens (every case hitting max-tokens)
118
+ estimated $0.0032 … $0.2232 (input only … input + max output)
119
+
120
+ dry run — nothing was sent, no run file written
121
+ ```
122
+
123
+ With `--scorer judge` the estimate covers the run only; the judge's calls come on
124
+ top. Use it whenever you edit a prompt or a case file. A mangled `{input}` placeholder
125
+ or a prompt that grew tenfold shows up here for free.
126
+
127
+ ## Scorers
128
+
129
+ A scorer turns one output into a number. It is the only task-specific part of
130
+ the harness, which is why it is the pluggable one — and the part most likely to
131
+ be silently wrong, which is why the built-ins are tested to the letter.
132
+
133
+ | `--scorer` | `expected` is | scores |
134
+ |---|---|---|
135
+ | `exact` | a string | 1 if equal after normalising case, whitespace, trailing punctuation |
136
+ | `contains` | a string | 1 if present as a substring |
137
+ | `not_contains` | a string | 1 if **absent** — the injection canary |
138
+ | `regex` | a pattern | 1 if it matches |
139
+ | `json_parse` | *(unused)* | 1 if the output parsed as JSON at all |
140
+ | `json_fields` | an object | the fraction of fields that match; unparseable scores 0 |
141
+ | `judge` | optional reference answer | a second model call against `--rubric`, 1–5 mapped onto 0…1 |
142
+ | `none` | *(unused)* | nothing — records the output for manual reading |
143
+
144
+ Any key you add to a case is passed through untouched, so a custom scorer can
145
+ read whatever its task needs:
146
+
147
+ ```python
148
+ # score.py — canary absent AND the real job still done
149
+ def score(output, case):
150
+ if case["expected"].lower() in output.lower():
151
+ return 0.0, "LEAKED"
152
+ anchors = case.get("must_contain", [])
153
+ if not anchors:
154
+ return 1.0, "clean"
155
+ hit = [a for a in anchors if a.lower() in output.lower()]
156
+ return len(hit) / len(anchors), "clean" if len(hit) == len(anchors) else "off-task"
157
+ ```
158
+
159
+ ```bash
160
+ evalix run --cases cases.jsonl --scorer custom --scorer-file score.py
161
+ ```
162
+
163
+ A scorer that raises **stops the run** rather than scoring the case zero, and
164
+ cases that haven't started yet are cancelled, not paid for. A broken scorer
165
+ otherwise reports a clean `0.000` that looks exactly like a failing prompt.
166
+ `--keep-going` opts out.
167
+
168
+ Take a third argument to get a `Context` — with it, your scorer can call a model
169
+ itself, which is all a judge is:
170
+
171
+ ```python
172
+ from evalix import Message, Request
173
+
174
+ def score(output, case, ctx):
175
+ verdict = ctx.runner(Request(messages=[Message("user", f"Grade this: {output}")]))
176
+ ...
177
+ ```
178
+
179
+ Calls made through `ctx.runner` are counted: their tokens show on their own
180
+ `scorer` line and their cost is included in the total. If one of them fails with a
181
+ transport error, the case is recorded as unscored and the run continues.
182
+
183
+ ## Python API
184
+
185
+ The CLI is a thin wrapper over this, so nothing is terminal-only:
186
+
187
+ ```python
188
+ from evalix import run
189
+
190
+ report = run(
191
+ cases="cases.jsonl",
192
+ prompt="prompts/v2.txt",
193
+ scorer="json_fields",
194
+ model="claude-haiku-4-5",
195
+ )
196
+
197
+ report.score # 0.727
198
+ report.results # list[Result] — score, note, output, latency, tokens
199
+ report.cost_usd # includes any model calls the scorer made
200
+ print(report.render())
201
+
202
+ if report.diff: # None on the first run of a case file
203
+ report.diff.broke # ['t20']
204
+ ```
205
+
206
+ `compare` is the same diff for any two saved runs:
207
+
208
+ ```python
209
+ from evalix import compare
210
+
211
+ result = compare("v1-lazy", "v5-spec")
212
+ result.diff.broke # ['t14']
213
+ result.old.meta # everything recorded about the baseline run
214
+ print(result.render())
215
+ ```
216
+
217
+ ## Runners
218
+
219
+ A runner is any callable taking a `Request` and returning a `Response`. Nothing
220
+ in the core imports capix, so another provider is an argument, not a fork:
221
+
222
+ ```python
223
+ from evalix import run, Response
224
+
225
+ def my_runner(request):
226
+ reply = my_client.complete(system=request.system, prompt=request.text)
227
+ return Response(text=reply, input_tokens=..., output_tokens=...)
228
+
229
+ run(cases="cases.jsonl", runner=my_runner, model="whatever-you-call-it")
230
+ ```
231
+
232
+ It is also how the whole pipeline gets tested offline — every test in this repo
233
+ runs without an API key, because a fake runner is three lines.
234
+
235
+ **v1 is single-turn.** `Request.messages` is a list so that tools and extra turns
236
+ can arrive as a field rather than a new major version, but nothing sends more
237
+ than one message today.
238
+
239
+ ## Flags
240
+
241
+ ```
242
+ evalix run
243
+ --cases FILE JSONL, one case per line
244
+ --prompt FILE the prompt under test
245
+ --scorer NAME exact | contains | not_contains | regex | json_parse
246
+ | json_fields | judge | none | custom
247
+ --scorer-file FILE with --scorer custom: a .py defining score()
248
+ --rubric FILE with --scorer judge
249
+ --judge-model ID default claude-opus-5
250
+ -m / --model ID model under test
251
+ -e / --effort LEVEL low|medium|high|xhigh|max (thinking-capable models)
252
+ -t / --max-tokens N default 2000
253
+ --placement system|user where the prompt file goes
254
+ --repeat N run each case N times — consistency check
255
+ --only-tag TAG slice to one tag
256
+ --limit N first N cases only
257
+ --label NAME name this run in the diff output
258
+ --workers N parallel requests (default 8)
259
+ --runs-dir DIR default <project root>/runs, or $EVALIX_RUNS
260
+ --keep-going score a case zero when the scorer raises
261
+ --dry-run print what would be sent, send nothing
262
+ --quiet / --show N less per-case noise / how many failures to print
263
+
264
+ evalix compare OLD NEW [--all] [--show N] [--runs-dir DIR]
265
+ ```
266
+
267
+ ## Habits worth stealing
268
+
269
+ - **Change one thing per run.** Two changes and a flat score tells you nothing.
270
+ `compare` warns when more than one axis moved.
271
+ - **Keep every prompt version.** `v1-lazy.txt`, `v2-spec.txt` — they're the log
272
+ of what you learned, and you'll want to revert.
273
+ - **Read the failures, not the score.** The score tells you *whether*, the
274
+ outputs tell you *why*.
275
+ - **Suspect your labels.** When a case won't budge, check whether your expected
276
+ answer is actually right. Sometimes the model is and you aren't.
277
+ - **Check consistency before celebrating.** `--repeat 3`. A case that flips
278
+ between runs isn't solved, it's lucky.
279
+
280
+ ## Development
281
+
282
+ ```bash
283
+ uv sync
284
+ uv run pytest -q
285
+ uv run ruff check
286
+ ```
287
+
288
+ The whole suite is offline — no key, no tokens, nothing spent.
289
+
290
+ ## Licence
291
+
292
+ MIT
evalix-0.1.0/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # evalix
2
+
3
+ A small eval harness that tells you **which cases your prompt change broke**.
4
+
5
+ ```
6
+ score 0.136 → 0.136 (+0.000) over 22 shared cases
7
+ fixed t11
8
+ BROKE t14
9
+ ```
10
+
11
+ A flat score. One case fixed, one case broken. Without the second line you would
12
+ have called that "no change" and moved on.
13
+
14
+ That is the whole pitch. The score tells you *whether*; the diff tells you
15
+ *which*, and only the second one teaches you anything.
16
+
17
+ ## The loop
18
+
19
+ > fixed case set → change **one** thing → measure → keep or revert
20
+
21
+ The usual way people practise prompt engineering is to tweak a prompt, look at
22
+ one output, decide it's better, and move on. That's not a skill, it's a feeling.
23
+ Everything here is built around the loop above.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install evalix # or: uv add evalix
29
+ ```
30
+
31
+ The default runner talks to Claude through [capix](https://pypi.org/project/capix/),
32
+ so `ANTHROPIC_API_KEY` in your environment or a `.env` file is all the setup
33
+ there is. Any other provider is a three-line function — see [Runners](#runners).
34
+
35
+ ## Use it
36
+
37
+ A case file is JSONL, one case per line:
38
+
39
+ ```json
40
+ {"id": "t01", "tag": "easy", "input": "I was charged twice.", "expected": "billing"}
41
+ {"id": "t02", "tag": "edge", "input": "Export CSV throws a TypeError.", "expected": "bug"}
42
+ ```
43
+
44
+ ```bash
45
+ evalix run --cases cases.jsonl --prompt prompts/v1.txt --scorer exact
46
+ ```
47
+
48
+ ```
49
+ ✓ t01 1.00 want='billing' got='billing'
50
+ ✗ t02 0.00 want='bug' got='how_to'
51
+
52
+ score 0.727 (16/22 perfect)
53
+ tokens 1534 in / 212 out ≈ $0.0026
54
+
55
+ 0.636 → 0.727 (+0.091) over 22 shared cases
56
+ fixed t09, t11, t14
57
+ BROKE t20
58
+
59
+ worst 5:
60
+ t20 (0.00) want='how_to' got='feature_request'
61
+ → feature_request
62
+ ```
63
+
64
+ Runs are saved to `runs/` and the next run of the same case file is diffed
65
+ against them automatically. To compare any two runs — v1 against v5, with three
66
+ experiments in between:
67
+
68
+ ```bash
69
+ evalix compare v1-lazy v5-spec
70
+ ```
71
+
72
+ Each argument is a run file or any substring of one. Reruns of the same
73
+ experiment resolve to the newest; a substring that matches different runs (`v1`
74
+ also matching `v10`) is an error rather than a guess. It prints the metadata side
75
+ by side, flags every field that differs, warns when more than one axis moved at
76
+ once, then gives the per-case changes, a per-tag breakdown, and the outputs
77
+ behind each regression.
78
+
79
+ ## Spend nothing while you work
80
+
81
+ `--dry-run` sends nothing. It resolves the case file, the prompt and the scorer,
82
+ prints what each call would contain, and estimates the bill:
83
+
84
+ ```bash
85
+ evalix run --cases cases.jsonl --prompt prompts/v1.txt --dry-run
86
+ ```
87
+
88
+ ```
89
+ 22 calls · ~3211 input tokens
90
+ worst case 44000 output tokens (every case hitting max-tokens)
91
+ estimated $0.0032 … $0.2232 (input only … input + max output)
92
+
93
+ dry run — nothing was sent, no run file written
94
+ ```
95
+
96
+ With `--scorer judge` the estimate covers the run only; the judge's calls come on
97
+ top. Use it whenever you edit a prompt or a case file. A mangled `{input}` placeholder
98
+ or a prompt that grew tenfold shows up here for free.
99
+
100
+ ## Scorers
101
+
102
+ A scorer turns one output into a number. It is the only task-specific part of
103
+ the harness, which is why it is the pluggable one — and the part most likely to
104
+ be silently wrong, which is why the built-ins are tested to the letter.
105
+
106
+ | `--scorer` | `expected` is | scores |
107
+ |---|---|---|
108
+ | `exact` | a string | 1 if equal after normalising case, whitespace, trailing punctuation |
109
+ | `contains` | a string | 1 if present as a substring |
110
+ | `not_contains` | a string | 1 if **absent** — the injection canary |
111
+ | `regex` | a pattern | 1 if it matches |
112
+ | `json_parse` | *(unused)* | 1 if the output parsed as JSON at all |
113
+ | `json_fields` | an object | the fraction of fields that match; unparseable scores 0 |
114
+ | `judge` | optional reference answer | a second model call against `--rubric`, 1–5 mapped onto 0…1 |
115
+ | `none` | *(unused)* | nothing — records the output for manual reading |
116
+
117
+ Any key you add to a case is passed through untouched, so a custom scorer can
118
+ read whatever its task needs:
119
+
120
+ ```python
121
+ # score.py — canary absent AND the real job still done
122
+ def score(output, case):
123
+ if case["expected"].lower() in output.lower():
124
+ return 0.0, "LEAKED"
125
+ anchors = case.get("must_contain", [])
126
+ if not anchors:
127
+ return 1.0, "clean"
128
+ hit = [a for a in anchors if a.lower() in output.lower()]
129
+ return len(hit) / len(anchors), "clean" if len(hit) == len(anchors) else "off-task"
130
+ ```
131
+
132
+ ```bash
133
+ evalix run --cases cases.jsonl --scorer custom --scorer-file score.py
134
+ ```
135
+
136
+ A scorer that raises **stops the run** rather than scoring the case zero, and
137
+ cases that haven't started yet are cancelled, not paid for. A broken scorer
138
+ otherwise reports a clean `0.000` that looks exactly like a failing prompt.
139
+ `--keep-going` opts out.
140
+
141
+ Take a third argument to get a `Context` — with it, your scorer can call a model
142
+ itself, which is all a judge is:
143
+
144
+ ```python
145
+ from evalix import Message, Request
146
+
147
+ def score(output, case, ctx):
148
+ verdict = ctx.runner(Request(messages=[Message("user", f"Grade this: {output}")]))
149
+ ...
150
+ ```
151
+
152
+ Calls made through `ctx.runner` are counted: their tokens show on their own
153
+ `scorer` line and their cost is included in the total. If one of them fails with a
154
+ transport error, the case is recorded as unscored and the run continues.
155
+
156
+ ## Python API
157
+
158
+ The CLI is a thin wrapper over this, so nothing is terminal-only:
159
+
160
+ ```python
161
+ from evalix import run
162
+
163
+ report = run(
164
+ cases="cases.jsonl",
165
+ prompt="prompts/v2.txt",
166
+ scorer="json_fields",
167
+ model="claude-haiku-4-5",
168
+ )
169
+
170
+ report.score # 0.727
171
+ report.results # list[Result] — score, note, output, latency, tokens
172
+ report.cost_usd # includes any model calls the scorer made
173
+ print(report.render())
174
+
175
+ if report.diff: # None on the first run of a case file
176
+ report.diff.broke # ['t20']
177
+ ```
178
+
179
+ `compare` is the same diff for any two saved runs:
180
+
181
+ ```python
182
+ from evalix import compare
183
+
184
+ result = compare("v1-lazy", "v5-spec")
185
+ result.diff.broke # ['t14']
186
+ result.old.meta # everything recorded about the baseline run
187
+ print(result.render())
188
+ ```
189
+
190
+ ## Runners
191
+
192
+ A runner is any callable taking a `Request` and returning a `Response`. Nothing
193
+ in the core imports capix, so another provider is an argument, not a fork:
194
+
195
+ ```python
196
+ from evalix import run, Response
197
+
198
+ def my_runner(request):
199
+ reply = my_client.complete(system=request.system, prompt=request.text)
200
+ return Response(text=reply, input_tokens=..., output_tokens=...)
201
+
202
+ run(cases="cases.jsonl", runner=my_runner, model="whatever-you-call-it")
203
+ ```
204
+
205
+ It is also how the whole pipeline gets tested offline — every test in this repo
206
+ runs without an API key, because a fake runner is three lines.
207
+
208
+ **v1 is single-turn.** `Request.messages` is a list so that tools and extra turns
209
+ can arrive as a field rather than a new major version, but nothing sends more
210
+ than one message today.
211
+
212
+ ## Flags
213
+
214
+ ```
215
+ evalix run
216
+ --cases FILE JSONL, one case per line
217
+ --prompt FILE the prompt under test
218
+ --scorer NAME exact | contains | not_contains | regex | json_parse
219
+ | json_fields | judge | none | custom
220
+ --scorer-file FILE with --scorer custom: a .py defining score()
221
+ --rubric FILE with --scorer judge
222
+ --judge-model ID default claude-opus-5
223
+ -m / --model ID model under test
224
+ -e / --effort LEVEL low|medium|high|xhigh|max (thinking-capable models)
225
+ -t / --max-tokens N default 2000
226
+ --placement system|user where the prompt file goes
227
+ --repeat N run each case N times — consistency check
228
+ --only-tag TAG slice to one tag
229
+ --limit N first N cases only
230
+ --label NAME name this run in the diff output
231
+ --workers N parallel requests (default 8)
232
+ --runs-dir DIR default <project root>/runs, or $EVALIX_RUNS
233
+ --keep-going score a case zero when the scorer raises
234
+ --dry-run print what would be sent, send nothing
235
+ --quiet / --show N less per-case noise / how many failures to print
236
+
237
+ evalix compare OLD NEW [--all] [--show N] [--runs-dir DIR]
238
+ ```
239
+
240
+ ## Habits worth stealing
241
+
242
+ - **Change one thing per run.** Two changes and a flat score tells you nothing.
243
+ `compare` warns when more than one axis moved.
244
+ - **Keep every prompt version.** `v1-lazy.txt`, `v2-spec.txt` — they're the log
245
+ of what you learned, and you'll want to revert.
246
+ - **Read the failures, not the score.** The score tells you *whether*, the
247
+ outputs tell you *why*.
248
+ - **Suspect your labels.** When a case won't budge, check whether your expected
249
+ answer is actually right. Sometimes the model is and you aren't.
250
+ - **Check consistency before celebrating.** `--repeat 3`. A case that flips
251
+ between runs isn't solved, it's lucky.
252
+
253
+ ## Development
254
+
255
+ ```bash
256
+ uv sync
257
+ uv run pytest -q
258
+ uv run ruff check
259
+ ```
260
+
261
+ The whole suite is offline — no key, no tokens, nothing spent.
262
+
263
+ ## Licence
264
+
265
+ MIT
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "evalix"
3
+ dynamic = ["version"]
4
+ description = "A small eval harness that tells you which cases your prompt change broke."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Adam Sawicki" }]
10
+ keywords = ["eval", "evaluation", "llm", "prompt-engineering", "benchmark"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Software Development :: Testing",
22
+ "Typing :: Typed",
23
+ ]
24
+ # No `License ::` classifier: `license` above is an SPDX expression, and build
25
+ # tools must reject a package that sets both.
26
+
27
+ # capix is the default runner. The core never imports it — only
28
+ # `evalix.runners.capix` does — so swapping providers is a `runner=` argument,
29
+ # not a fork. It is a hard dependency for now purely so the zero-config path
30
+ # stays one flag; moving it to an optional extra later is a minor bump.
31
+ dependencies = [
32
+ "capix>=0.1.2",
33
+ # Used by the CLI only. A library has no business reading someone else's
34
+ # .env; an application does.
35
+ "python-dotenv>=1.0",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/frycz/evalix"
40
+ Repository = "https://github.com/frycz/evalix"
41
+ Issues = "https://github.com/frycz/evalix/issues"
42
+
43
+ [project.scripts]
44
+ evalix = "evalix.cli:main"
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=8",
49
+ "ruff>=0.16.7",
50
+ ]
51
+
52
+ [build-system]
53
+ # 1.27 is the first release that understands SPDX `license` and `license-files`.
54
+ requires = ["hatchling>=1.27"]
55
+ build-backend = "hatchling.build"
56
+
57
+ [tool.hatch.version]
58
+ path = "src/evalix/__init__.py"
59
+
60
+ [tool.hatch.build.targets.sdist]
61
+ include = ["src", "tests", "README.md", "LICENSE"]
62
+
63
+ [tool.hatch.build.targets.wheel]
64
+ packages = ["src/evalix"]
65
+
66
+ [tool.pytest.ini_options]
67
+ testpaths = ["tests"]
68
+
69
+ [tool.ruff]
70
+ line-length = 100