evalcore 0.1.0__tar.gz → 0.2.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 (78) hide show
  1. {evalcore-0.1.0 → evalcore-0.2.0}/.github/workflows/ci.yml +13 -1
  2. evalcore-0.2.0/CHANGELOG.md +35 -0
  3. evalcore-0.1.0/README.md → evalcore-0.2.0/PKG-INFO +137 -77
  4. evalcore-0.1.0/PKG-INFO → evalcore-0.2.0/README.md +105 -102
  5. {evalcore-0.1.0 → evalcore-0.2.0}/docs/design.md +23 -23
  6. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/README.md +7 -7
  7. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/adapter.py +2 -2
  8. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/graders.py +2 -2
  9. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/run_eval.py +3 -3
  10. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/suite.yaml +3 -3
  11. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/tests/test_quickstart.py +1 -1
  12. {evalcore-0.1.0 → evalcore-0.2.0}/pyproject.toml +16 -11
  13. evalcore-0.2.0/src/evalcore/__init__.py +88 -0
  14. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/adapters/__init__.py +3 -2
  15. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/adapters/base.py +4 -3
  16. evalcore-0.1.0/src/evalkit/adapters/_env.py → evalcore-0.2.0/src/evalcore/adapters/env.py +11 -6
  17. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/adapters/http.py +5 -5
  18. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/adapters/replay.py +2 -2
  19. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/cli.py +47 -23
  20. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/compare.py +1 -1
  21. evalcore-0.2.0/src/evalcore/errors.py +19 -0
  22. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/__init__.py +7 -1
  23. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/base.py +4 -3
  24. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/classification.py +2 -2
  25. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/deterministic.py +2 -2
  26. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/judge.py +6 -9
  27. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/graders/numeric.py +3 -3
  28. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/loader.py +2 -2
  29. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/pairwise.py +4 -7
  30. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/rating.py +10 -13
  31. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/report.py +1 -1
  32. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/reporters/__init__.py +10 -3
  33. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/reporters/base.py +23 -10
  34. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/reporters/html.py +59 -4
  35. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/reporters/markdown.py +10 -4
  36. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/runner.py +4 -4
  37. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/store.py +1 -1
  38. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/sweep.py +1 -1
  39. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_adapters.py +3 -3
  40. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_cli.py +2 -2
  41. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_edge_cases.py +4 -4
  42. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_judge.py +3 -3
  43. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_judge_extra.py +2 -2
  44. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_live_clients.py +2 -2
  45. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_pairwise_extra.py +1 -1
  46. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_rating.py +1 -1
  47. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_rating_server.py +2 -2
  48. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_reporters.py +89 -2
  49. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_retry.py +1 -1
  50. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_runner.py +3 -3
  51. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_store.py +1 -1
  52. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_sweep_pairwise.py +1 -1
  53. {evalcore-0.1.0 → evalcore-0.2.0}/tests/test_unit.py +5 -5
  54. evalcore-0.2.0/uv.lock +755 -0
  55. evalcore-0.1.0/src/evalkit/__init__.py +0 -39
  56. evalcore-0.1.0/uv.lock +0 -510
  57. {evalcore-0.1.0 → evalcore-0.2.0}/.github/workflows/publish.yml +0 -0
  58. {evalcore-0.1.0 → evalcore-0.2.0}/.gitignore +0 -0
  59. {evalcore-0.1.0 → evalcore-0.2.0}/.pre-commit-config.yaml +0 -0
  60. {evalcore-0.1.0 → evalcore-0.2.0}/LICENSE +0 -0
  61. {evalcore-0.1.0 → evalcore-0.2.0}/examples/__init__.py +0 -0
  62. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/__init__.py +0 -0
  63. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/datasets/support_reply/v1/cases/angry_complaint.yaml +0 -0
  64. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/datasets/support_reply/v1/cases/billing_question.yaml +0 -0
  65. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/datasets/support_reply/v1/cases/double_charge.yaml +0 -0
  66. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/datasets/support_reply/v1/cases/refund_request.yaml +0 -0
  67. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/fixtures/support_reply_judge.yaml +0 -0
  68. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/fixtures/support_reply_pairwise.yaml +0 -0
  69. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/fixtures/support_reply_replay.yaml +0 -0
  70. {evalcore-0.1.0 → evalcore-0.2.0}/examples/quickstart/tests/__init__.py +0 -0
  71. {evalcore-0.1.0 → evalcore-0.2.0}/justfile +0 -0
  72. {evalcore-0.1.0 → evalcore-0.2.0}/pyrightconfig.json +0 -0
  73. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/models.py +0 -0
  74. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/py.typed +0 -0
  75. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/refs.py +0 -0
  76. {evalcore-0.1.0/src/evalkit → evalcore-0.2.0/src/evalcore}/retry.py +0 -0
  77. {evalcore-0.1.0 → evalcore-0.2.0}/tests/__init__.py +0 -0
  78. {evalcore-0.1.0 → evalcore-0.2.0}/uv.toml +0 -0
@@ -10,13 +10,17 @@ on:
10
10
  jobs:
11
11
  test:
12
12
  runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
13
17
  steps:
14
18
  - uses: actions/checkout@v4
15
19
 
16
20
  - name: Install uv
17
21
  uses: astral-sh/setup-uv@v5
18
22
  with:
19
- python-version: "3.14"
23
+ python-version: ${{ matrix.python-version }}
20
24
 
21
25
  - name: Sync dependencies
22
26
  run: uv sync --all-extras
@@ -31,5 +35,13 @@ jobs:
31
35
  uv run coverage run
32
36
  uv run coverage report
33
37
 
38
+ build:
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - name: Install uv
43
+ uses: astral-sh/setup-uv@v5
44
+ with:
45
+ python-version: "3.14"
34
46
  - name: Build wheel + sdist
35
47
  run: uv build
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/), and the project follows
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Changed
10
+ - **Breaking:** the import package and CLI are now `evalcore` (were `evalkit`).
11
+ Update `import evalkit` to `import evalcore` and the `evalkit` command to
12
+ `evalcore`. The distribution name (`evalcore`) is unchanged.
13
+ - Lowered the minimum Python to **3.11** (was 3.14).
14
+
15
+ ### Added
16
+ - `evalcore.__version__`.
17
+ - Public `evalcore.adapters.expand_env` for `${VAR}` expansion in custom
18
+ adapters (replaces the private `adapters._env` module).
19
+ - Exception hierarchy: `EvalcoreError` (base) and `ConfigError` (also a
20
+ `ValueError`, so existing handlers keep working).
21
+ - Top-level convenience entry points: `load_suite`, `load_cases`, `run_suite`,
22
+ `run_suite_sync`.
23
+ - HTML rendering for `sweep` and `pairwise` reports, and `--report` /
24
+ `--report-out` on those CLI commands.
25
+
26
+ ## [0.1.0] - 2026-07-16
27
+
28
+ - Initial public release: adapters (http/replay), graders (deterministic,
29
+ numeric, classification, LLM judge + panel), runner (N-sampling, concurrency,
30
+ retries, checkpoint/resume), compare/gate, sweep, pairwise, blind human
31
+ rating + ranking with judge agreement, Markdown/HTML reporters, JSON +
32
+ column-store outbox, and content-hash provenance.
33
+
34
+ [Unreleased]: https://github.com/scottpmiller/evalcore/compare/0.1.0...HEAD
35
+ [0.1.0]: https://github.com/scottpmiller/evalcore/releases/tag/0.1.0
@@ -1,14 +1,46 @@
1
- # evalkit
1
+ Metadata-Version: 2.4
2
+ Name: evalcore
3
+ Version: 0.2.0
4
+ Summary: A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs.
5
+ Project-URL: Homepage, https://github.com/scottpmiller/evalcore
6
+ Project-URL: Repository, https://github.com/scottpmiller/evalcore
7
+ Project-URL: Issues, https://github.com/scottpmiller/evalcore/issues
8
+ Author: AWeber Communications
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: pydantic<3,>=2.7
25
+ Requires-Dist: pyyaml<7,>=6
26
+ Provides-Extra: http
27
+ Requires-Dist: httpx>=0.27; extra == 'http'
28
+ Provides-Extra: judge
29
+ Requires-Dist: anthropic>=0.40; extra == 'judge'
30
+ Requires-Dist: openai>=1.40; extra == 'judge'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # evalcore
2
34
 
3
35
  A small, **consumer-agnostic** evaluation engine for prompt, model, and API
4
36
  outputs. It detects regressions and improvements as prompts and models change,
5
37
  by scoring a candidate against a baseline over a fixed dataset and applying
6
38
  guardrails + a headline win metric to produce a gate verdict.
7
39
 
8
- evalkit knows nothing about any particular system under test. A consumer
9
- supplies four things; evalkit supplies everything else:
40
+ evalcore knows nothing about any particular system under test. A consumer
41
+ supplies four things; evalcore supplies everything else:
10
42
 
11
- | Consumer provides (data + small plug-ins) | evalkit provides |
43
+ | Consumer provides (data + small plug-ins) | evalcore provides |
12
44
  | --- | --- |
13
45
  | an **adapter** config (how to call the system + the knobs a variant sets) | runner (N-sampling), comparison/regression engine |
14
46
  | **datasets** (cases with opaque `input`/`expected` blobs) | grader registry + generic graders |
@@ -23,12 +55,38 @@ offline) and doubles as an end-to-end usage reference.
23
55
  ## Install
24
56
 
25
57
  ```bash
26
- pip install evalcore # distribution name; imports as `evalkit`
58
+ pip install evalcore
27
59
  ```
28
60
 
29
- The PyPI package is **`evalcore`** (the name `evalkit` was taken); the import
30
- package and CLI are still `evalkit` (`import evalkit`, `evalkit --help`). Extras:
31
- `evalcore[http]`, `evalcore[judge]`.
61
+ Imports as `evalcore` (`import evalcore`); CLI is `evalcore` (`evalcore --help`).
62
+ Extras: `evalcore[http]` (live HTTP adapter), `evalcore[judge]` (live LLM judge).
63
+
64
+ ## Quickstart
65
+
66
+ Score a candidate against a baseline over a suite and read the gate verdict -
67
+ fully offline against recorded fixtures, so it runs with no network or API keys:
68
+
69
+ ```python
70
+ import evalcore as ec
71
+
72
+ suite = ec.load_suite('suite.yaml')
73
+ baseline = ec.run_suite_sync(suite, 'baseline', mode='replay').scorecard
74
+ candidate = ec.run_suite_sync(suite, 'candidate', mode='replay').scorecard
75
+
76
+ result = ec.compare.compare(baseline, candidate, suite.thresholds)
77
+ print(result.verdict) # 'pass' | 'warn' | 'fail'
78
+ print(ec.report.render_comparison(result)) # Markdown diff + guardrails
79
+ ```
80
+
81
+ Or from the command line (exits non-zero on `fail`, so it drops into CI):
82
+
83
+ ```bash
84
+ evalcore gate --suite suite.yaml --mode replay
85
+ ```
86
+
87
+ `examples/quickstart/` is a complete, runnable version of the above (custom
88
+ adapter + graders + suite). The rest of this README is the reference for
89
+ writing your own.
32
90
 
33
91
  ## Develop
34
92
 
@@ -69,7 +127,7 @@ The five steps, in dependency order:
69
127
  ## 1. Cases (the dataset)
70
128
 
71
129
  A dataset is a directory containing `cases/`, holding one YAML (or JSON)
72
- file per case. Every field except `id` is **opaque to the engine** — only
130
+ file per case. Every field except `id` is **opaque to the engine** - only
73
131
  your adapter and graders interpret `input` and `expected`:
74
132
 
75
133
  ```yaml
@@ -99,7 +157,7 @@ class TargetAdapter(typing.Protocol):
99
157
  ```
100
158
 
101
159
  It receives one `Case` and one `Variant` and must return an
102
- `Output` — never raise for a failed invocation; set `Output.error` instead
160
+ `Output` - never raise for a failed invocation; set `Output.error` instead
103
161
  so graders can count it (see `errors` below). Set `Output.retryable = True`
104
162
  alongside a *transient* error (a 429, a 5xx, a network timeout) and the
105
163
  runner's retry loop will back off and try again (see "Retries" below); leave
@@ -137,15 +195,15 @@ the latency still recorded.
137
195
 
138
196
  ### Custom adapters
139
197
 
140
- When the built-in isn't enough — auth dances, response post-processing,
141
- non-HTTP targets — register your own under a config `type` and select it in
198
+ When the built-in isn't enough - auth dances, response post-processing,
199
+ non-HTTP targets - register your own under a config `type` and select it in
142
200
  the suite. Subclassing the http adapter is often the shortest path.
143
201
  `examples/quickstart/adapter.py` is a worked example: it turns each case's
144
202
  input into structured `Output.fields` your graders can score:
145
203
 
146
204
  ```python
147
- from evalkit import models
148
- from evalkit.adapters import base, http
205
+ from evalcore import models
206
+ from evalcore.adapters import base, http
149
207
 
150
208
  @base.register('my_service_json')
151
209
  class MyAdapter(http.HTTPAdapter):
@@ -157,20 +215,20 @@ class MyAdapter(http.HTTPAdapter):
157
215
 
158
216
  Constructor kwargs come from the suite's `adapter:` mapping (everything
159
217
  except `type`). Load the module at run time with `--plugins my_service.graders`
160
- (CLI) or a plain `import` (Python API) — registration happens on import.
218
+ (CLI) or a plain `import` (Python API) - registration happens on import.
161
219
 
162
220
  An adapter need not be HTTP-backed: it can grade *what a deployed system
163
- already did* by reading from an observability store — turning an aggregated
221
+ already did* by reading from an observability store - turning an aggregated
164
222
  result row into `Output.fields`.
165
223
 
166
224
  An adapter that holds resources (a browser, an injected session, pooled
167
225
  connections) may expose an optional async `aclose()`; the runner calls it
168
- after the run, even on failure — so a browser-automation adapter (e.g. one
226
+ after the run, even on failure - so a browser-automation adapter (e.g. one
169
227
  driving Playwright) can open its context once and tear it down cleanly.
170
228
 
171
229
  ## 3. Variants (what's being compared)
172
230
 
173
- A variant is a named dict of **knobs** — opaque to the engine, interpreted
231
+ A variant is a named dict of **knobs** - opaque to the engine, interpreted
174
232
  by your adapter (usually via `$variant.*` refs in the body template):
175
233
 
176
234
  ```yaml
@@ -265,17 +323,17 @@ metrics: only `Score`s reach a scorecard, so a value the adapter merely put in
265
323
  field's metric name defaults to the ref's leaf (`output.cost` -> `cost`); add
266
324
  `min`/`max` to also emit per-case pass/fail. Absent or non-numeric fields
267
325
  degrade to `null`. `compare`'s guardrails and a `win_metric` with
268
- `win_higher_is_better: false` then gate on these directly — e.g. gating a
326
+ `win_higher_is_better: false` then gate on these directly - e.g. gating a
269
327
  `generation_cost` or `tool_error_rate` alongside quality judges.
270
328
 
271
329
  The judge runs live (`AnthropicJudgeClient` forced tool call, or
272
- `OpenAIJudgeClient` `json_schema` — both temperature 0, needing the `judge`
330
+ `OpenAIJudgeClient` `json_schema` - both temperature 0, needing the `judge`
273
331
  extra plus `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`) or offline
274
332
  (`ReplayJudgeClient`), chosen by the run mode like the adapter. Each
275
333
  dimension becomes a metric `<name>.<key>` plus a `<name>.overall` mean.
276
334
 
277
335
  **Panel + images.** Replace the single `model`/`replay_path` with a
278
- `judges:` list to run a **panel** — each judge scores independently:
336
+ `judges:` list to run a **panel** - each judge scores independently:
279
337
 
280
338
  ```yaml
281
339
  - type: llm_judge
@@ -294,14 +352,14 @@ A panel emits, on top of the per-dimension panel means and `<name>.overall`:
294
352
  `<name>.<judge>.overall` (each judge's own mean, so a systematically
295
353
  generous judge is visible), `<name>.disagreement` (mean inter-judge spread
296
354
  in raw points), and `<name>.flagged` (1.0 when any dimension's spread
297
- reaches `disagreement_threshold` — averaged across cases, the fraction a
355
+ reaches `disagreement_threshold` - averaged across cases, the fraction a
298
356
  human should review). `image_refs` resolve to file paths (from
299
357
  `output.artifacts`) or inline `{media_type, data}`; images are sent live
300
358
  only, so replay stays offline. A single judge emits none of the panel-only
301
359
  metrics, so existing single-judge suites are unchanged (a panel is the natural
302
360
  fit for judging rendered screenshots with a Claude+GPT pair, for instance).
303
361
 
304
- Custom graders register exactly like adapters — see
362
+ Custom graders register exactly like adapters - see
305
363
  `examples/quickstart/graders.py` for one of each kind (a per-case
306
364
  keyword check and a whole-run distinctness check). A grader that needs to
307
365
  know the run mode (live vs replay) can expose `set_mode(mode: str)`; the
@@ -310,7 +368,7 @@ runner calls it before the run starts.
310
368
  ## 5. Fixtures (offline / CI runs)
311
369
 
312
370
  Replay mode swaps the configured adapter for recorded outputs, keyed by
313
- case id and variant name — the whole pipeline then runs with no network,
371
+ case id and variant name - the whole pipeline then runs with no network,
314
372
  keys, or deployed service:
315
373
 
316
374
  ```yaml
@@ -384,56 +442,58 @@ reported informationally.
384
442
 
385
443
  ```bash
386
444
  # one variant -> scorecard (optionally saved)
387
- evalkit --plugins my_service.graders run \
445
+ evalcore --plugins my_service.graders run \
388
446
  --suite my_service/suite.yaml --variant candidate --mode replay \
389
447
  --out candidate.scorecard.json --revision "$GIT_SHA"
390
448
 
391
449
  # the CI workhorse: run baseline+candidate, compare, exit 1 on 'fail'
392
- evalkit --plugins my_service.graders gate \
450
+ evalcore --plugins my_service.graders gate \
393
451
  --suite my_service/suite.yaml --mode replay \
394
452
  --export outbox.jsonl --revision "$GIT_SHA"
395
453
 
396
454
  # re-compare two previously saved runs (accepts --out scorecards OR
397
455
  # --run-out run files)
398
- evalkit compare --suite my_service/suite.yaml \
456
+ evalcore compare --suite my_service/suite.yaml \
399
457
  --baseline old.run.json --candidate new.run.json
400
458
  ```
401
459
 
402
460
  `gate` picks variant names from `thresholds.variants`, falling back to
403
461
  variants literally named `baseline`/`candidate`. `--revision` is an opaque
404
- provenance id (git SHA, image digest, release label — whatever your world
462
+ provenance id (git SHA, image digest, release label - whatever your world
405
463
  uses; the engine never interprets it).
406
464
 
407
- `run`, `compare`, and `gate` take `--report markdown` (default) or
408
- `--report html` for a standalone, self-contained report document (a CI
409
- artifact or PR attachment). Reporters are a registry seam like adapters and
410
- graders — register a custom format with `evalkit.reporters.base.register`
411
- and select it by name, e.g. `--report pdf`.
465
+ `run`, `compare`, `gate`, `sweep`, and `pairwise` take `--report markdown`
466
+ (default) or `--report html` for a standalone, self-contained report document
467
+ (a CI artifact or PR attachment), plus `--report-out FILE` to write it instead
468
+ of printing. Reporters are a registry seam like adapters and graders - register
469
+ a custom format with `evalcore.reporters.base.register` and select it by name,
470
+ e.g. `--report pdf`. Every report type is also available as a plain library
471
+ call (`evalcore.reporters` / `evalcore.report`), returning the rendered string.
412
472
 
413
- **The change loop — run, change, run, compare.** To measure whether a
473
+ **The change loop - run, change, run, compare.** To measure whether a
414
474
  change (a prompt edit, a new model, a frontend PR) helped or regressed,
415
475
  run the *same variant* before and after and compare the two saved runs:
416
476
 
417
477
  ```bash
418
- evalkit ... run --variant candidate --run-out before.run.json --revision before
478
+ evalcore ... run --variant candidate --run-out before.run.json --revision before
419
479
  # ... make the change (edit the prompt, point at the PR build, swap the model) ...
420
- evalkit ... run --variant candidate --run-out after.run.json --revision after
421
- evalkit compare --suite my_service/suite.yaml \
480
+ evalcore ... run --variant candidate --run-out after.run.json --revision after
481
+ evalcore compare --suite my_service/suite.yaml \
422
482
  --baseline before.run.json --candidate after.run.json # deltas + verdict
423
483
  ```
424
484
 
425
485
  The comparison's guardrails + win metric then read as "did the change
426
486
  regress?" For nondeterministic targets (LLMs, browsers) raise `n_samples`
427
- so each metric is a mean (with `stdev`) over several generations — a single
487
+ so each metric is a mean (with `stdev`) over several generations - a single
428
488
  run per side makes a small delta indistinguishable from run-to-run noise,
429
489
  and `win_min_delta` is your noise floor.
430
490
 
431
- **Python API** — everything the CLI does is a library call; the full worked
491
+ **Python API** - everything the CLI does is a library call; the full worked
432
492
  version is `examples/quickstart/run_eval.py`:
433
493
 
434
494
  ```python
435
495
  import my_service.graders # noqa: F401 (registers custom types)
436
- from evalkit import compare, loader, report, runner, store
496
+ from evalcore import compare, loader, report, runner, store
437
497
 
438
498
  suite = loader.load_suite('my_service/suite.yaml')
439
499
  baseline = runner.run_suite_sync(suite, 'baseline', mode='replay',
@@ -479,8 +539,8 @@ observation passed; `0.9167` = 11 of 12. The metric name is the grader's
479
539
  rubric-scored 1..scale by the pinned judge, normalized to 0..1, averaged
480
540
  across observations. `overall` is the per-output mean of the dimensions,
481
541
  then averaged. An output the judge couldn't score (errored invocation,
482
- missing content) contributes *nothing* — it is excluded from the mean, not
483
- counted as zero — so always read judge means alongside `errors`.
542
+ missing content) contributes *nothing* - it is excluded from the mean, not
543
+ counted as zero - so always read judge means alongside `errors`.
484
544
 
485
545
  **Set-level aggregates** (classification + custom aggregate graders).
486
546
  Computed once over the whole run from a confusion matrix. The
@@ -497,7 +557,7 @@ stray verdict can never masquerade as a catch. Then:
497
557
  | `f1` | 2PR/(P+R) | single-number balance of the two |
498
558
  | `false_negative_rate` | FN/(FN+TP) | misses, as a fraction of real positives |
499
559
  | `false_positive_rate` | FP/(FP+TN) | false alarms, as a fraction of real negatives |
500
- | `accuracy` | (TP+TN)/all | overall fraction correct — flatters on imbalanced data; never guardrail it |
560
+ | `accuracy` | (TP+TN)/all | overall fraction correct - flatters on imbalanced data; never guardrail it |
501
561
  | `support_positive` / `support_negative` | TP+FN / TN+FP | the denominators: how much evidence backs the rates |
502
562
  | `errors` | count | invocations that failed or produced no usable label |
503
563
 
@@ -507,7 +567,7 @@ slipped through", FPR = "a negative got blocked". They have different
507
567
  denominators, so they stay honest on imbalanced datasets where `accuracy`
508
568
  lies. `support_*` doubles as a drift alarm: if it changes between runs on
509
569
  the same `dataset_hash`, extraction or labels broke. `errors` is a raw
510
- count and worth a `max: 0` guardrail — errored results are excluded from
570
+ count and worth a `max: 0` guardrail - errored results are excluded from
511
571
  every rate, so without it a variant that crashes on its hardest cases
512
572
  would look *better*.
513
573
 
@@ -526,7 +586,7 @@ is not the combined F1).
526
586
  The badge is the verdict; the tail is why. The delta table lists every
527
587
  metric with baseline / candidate / delta; the row marked
528
588
  **(improved | regressed | neutral)** is the configured win metric. Its call
529
- uses the dead band: `|delta| <= win_min_delta` → **neutral** — deliberate
589
+ uses the dead band: `|delta| <= win_min_delta` → **neutral** - deliberate
530
590
  protection against celebrating (or reverting on) noise from small samples.
531
591
 
532
592
  The **Guardrails** section shows each rule as `[ok]` or `[BREACH]` with the
@@ -539,7 +599,7 @@ measured value. Verdict logic, in order:
539
599
 
540
600
  `gate` (and the example driver) exit non-zero exactly on **FAIL**, so the
541
601
  verdict drops straight into CI. A **PASS with neutral win** is a perfectly
542
- good outcome — it means "no regression, no proven improvement".
602
+ good outcome - it means "no regression, no proven improvement".
543
603
 
544
604
  ## Provenance (trusting a number later)
545
605
 
@@ -550,19 +610,19 @@ judge_version, revision, suite_hash, dataset_hash, mode, created_at`.
550
610
  The declared versions state *intent*; the engine-computed content hashes
551
611
  prove it: `suite_hash` digests the raw suite file, `dataset_hash` digests
552
612
  the loaded cases (order-independent, formatting-independent). Two runs
553
- whose hashes match evaluated the same config over the same data — if a
613
+ whose hashes match evaluated the same config over the same data - if a
554
614
  metric moved, the *system under test* moved. If a hash changed, the eval
555
615
  itself changed and the comparison is apples-to-oranges: re-baseline.
556
616
  `revision` ties the run to whatever provenance scheme you use (commit,
557
617
  image digest, release label). A judge model/prompt/scale change is also a
558
- re-baseline event — bump `judge_version`; the runner lifts each judge
618
+ re-baseline event - bump `judge_version`; the runner lifts each judge
559
619
  grader's pin (`key@version`, a panel joins them) onto `Scorecard.judge_version`
560
620
  so it rides the reproducibility key, and it stays recorded on every judge
561
621
  score's `detail` too.
562
622
 
563
623
  ## Per-sample results
564
624
 
565
- `runner.run_suite` returns a **`RunResult`** — the scorecard plus every
625
+ `runner.run_suite` returns a **`RunResult`** - the scorecard plus every
566
626
  per-sample `CaseResult` (the output, its `artifacts`, and its scores). The
567
627
  scorecard is the aggregate; the results are the ground truth it was folded
568
628
  from. Persist the whole thing with `store.write_run` (CLI: `run --run-out`)
@@ -575,10 +635,10 @@ observations, so repeat-generation spread is visible, not just the average.
575
635
  Set `concurrency: N` in the suite to run invocations concurrently (the
576
636
  adapter and per-case graders must then tolerate concurrent calls).
577
637
 
578
- **Retries.** Live targets fail transiently — a rate limit, a 5xx, a dropped
638
+ **Retries.** Live targets fail transiently - a rate limit, a 5xx, a dropped
579
639
  connection. A `retry:` block (above) makes the runner re-invoke the adapter
580
640
  with exponential backoff (`backoff_base * 2**(attempt-1)`, capped at
581
- `backoff_max`, ± `jitter`) when — and only when — the adapter marks the
641
+ `backoff_max`, ± `jitter`) when - and only when - the adapter marks the
582
642
  failure `Output.retryable`. The built-in `http` adapter flags 429/5xx/network
583
643
  errors and leaves other 4xx terminal; a custom adapter sets the flag for
584
644
  whatever its transient failures are. The default (`max_attempts: 1`) is a
@@ -596,14 +656,14 @@ trail. Re-run with `--resume` and it reuses the recorded results and invokes
596
656
  only what's missing, reusing the original `run_id`:
597
657
 
598
658
  ```bash
599
- evalkit run --suite suite.yaml --variant candidate --checkpoint run.ckpt
659
+ evalcore run --suite suite.yaml --variant candidate --checkpoint run.ckpt
600
660
  # ... interrupted after 40/100 cases ...
601
- evalkit run --suite suite.yaml --variant candidate --checkpoint run.ckpt --resume
661
+ evalcore run --suite suite.yaml --variant candidate --checkpoint run.ckpt --resume
602
662
  ```
603
663
 
604
664
  The checkpoint's meta line records `suite_hash`/`dataset_hash`, so a resume
605
665
  against a changed suite, dataset, or variant refuses rather than mixing
606
- incompatible results — delete the checkpoint to start over (which `--resume`
666
+ incompatible results - delete the checkpoint to start over (which `--resume`
607
667
  also does implicitly when the file is absent). A checkpointed `(case, sample)`
608
668
  is treated as done whether it succeeded or errored; to redo just the failures,
609
669
  drop their lines from the checkpoint first.
@@ -611,29 +671,29 @@ drop their lines from the checkpoint first.
611
671
  ## Human rating & judge calibration
612
672
 
613
673
  An LLM judge is only trustworthy as a win metric once you've checked it
614
- tracks human taste. evalkit closes that loop over the persisted runs:
674
+ tracks human taste. evalcore closes that loop over the persisted runs:
615
675
 
616
676
  ```bash
617
677
  # blind rating web app over one or more saved runs (repeat --run to blind
618
678
  # across variants: the browser never sees which model produced an output)
619
- evalkit rate --run cand.run.json --run base.run.json \
679
+ evalcore rate --run cand.run.json --run base.run.json \
620
680
  --ratings ratings.jsonl --dimensions visual_design,copy_quality \
621
681
  --content-ref output.html # or a screenshot via artifacts.*
622
682
 
623
683
  # how well the judge agreed with the humans, per dimension
624
- evalkit agreement --run cand.run.json --ratings ratings.jsonl \
684
+ evalcore agreement --run cand.run.json --ratings ratings.jsonl \
625
685
  --dimensions visual_design,copy_quality --judge-name quality
626
686
  ```
627
687
 
628
688
  `rate` serves a dependency-free localhost page: a seeded-shuffled queue and
629
689
  1..scale buttons per dimension. It renders each item as **typed panels**
630
- derived from the output — `image`/`pdf` artifacts, `html` (sandboxed iframe
631
- with a rendered/source toggle), `json`, or `text` — so plain-text
690
+ derived from the output - `image`/`pdf` artifacts, `html` (sandboxed iframe
691
+ with a rendered/source toggle), `json`, or `text` - so plain-text
632
692
  or JSON results render with zero config, and any number of artifacts become
633
693
  that many panels. `--content-ref`/`--screenshot-ref` are the common
634
694
  shorthand; a repeatable `--view label:kind:ref` gives explicit control.
635
695
  Sessions are **resumable** (a rater only sees items they haven't scored).
636
- **Blinding is enforced server-side** — the queue payload carries an opaque
696
+ **Blinding is enforced server-side** - the queue payload carries an opaque
637
697
  item id and never the run/variant/model; ratings map back to
638
698
  `(run_id, case_id, sample_idx)` only on the server. Ratings land in a JSONL
639
699
  file (`models.Rating`) that is the **open interchange format**: any external
@@ -646,36 +706,36 @@ dimension as a win metric; a panel's `flagged` cases (see the judge panel
646
706
  above) are the natural first items to route through `rate`.
647
707
 
648
708
  **Side-by-side preference (`rank`/`preferences`).** `rate` scores each output
649
- in isolation; `rank` is its A-vs-B analog — the human counterpart of
709
+ in isolation; `rank` is its A-vs-B analog - the human counterpart of
650
710
  `pairwise`. It shows both variants' outputs for the same case as neutral
651
711
  "Option 1"/"Option 2" columns and the rater picks a winner overall and per
652
712
  dimension:
653
713
 
654
714
  ```bash
655
715
  # blind side-by-side ranking web app over two saved runs
656
- evalkit rank --run-a base.run.json --run-b cand.run.json \
716
+ evalcore rank --run-a base.run.json --run-b cand.run.json \
657
717
  --preferences prefs.jsonl --dimensions visual_design,copy_quality \
658
718
  --content-ref output.html
659
719
 
660
720
  # human A-vs-B win-rate (overall + per dimension) from the collected file
661
- evalkit preferences --run-a base.run.json --run-b cand.run.json \
721
+ evalcore preferences --run-a base.run.json --run-b cand.run.json \
662
722
  --preferences prefs.jsonl --report html --report-out prefs.html
663
723
  ```
664
724
 
665
725
  Left/right sides are **shuffled per rater and un-blinded server-side**, so a
666
726
  stored pick is always in *variant* terms (`variant_a`/`variant_b`) regardless
667
- of which side it was shown on — position bias counterbalances across raters
727
+ of which side it was shown on - position bias counterbalances across raters
668
728
  exactly like `pairwise`'s order swap. Sessions are resumable and picks land
669
729
  in a JSONL file (`models.Preference`), the **open A-vs-B interchange format**.
670
730
  `preferences` aggregates it (ties count half); to check the LLM pairwise judge
671
731
  against the human panel, pass the same file to `pairwise --preferences
672
- prefs.jsonl`, which appends a per-case human-vs-judge agreement table — the
732
+ prefs.jsonl`, which appends a per-case human-vs-judge agreement table - the
673
733
  head-to-head calibration gate.
674
734
 
675
735
  **Live judges on recorded data.** `--mode` drives the adapter; `--judge-mode`
676
736
  drives the graders independently (default: same as `--mode`). So
677
737
  `--mode replay --judge-mode live` re-scores recorded outputs with the live
678
- judge panel — no regeneration — which is how you iterate on a rubric or
738
+ judge panel - no regeneration - which is how you iterate on a rubric or
679
739
  re-record judge fixtures cheaply.
680
740
 
681
741
  ## Sweeps & pairwise win-rate
@@ -684,20 +744,20 @@ re-record judge fixtures cheaply.
684
744
 
685
745
  ```bash
686
746
  # N-way: run every variant (or a subset) and rank them by the win metric
687
- evalkit sweep --suite suite.yaml --mode replay # or --variants a,b,c
747
+ evalcore sweep --suite suite.yaml --mode replay # or --variants a,b,c
688
748
 
689
749
  # A-vs-B: a judge picks a winner per case -> A's win-rate
690
- evalkit pairwise --suite suite.yaml --a baseline --b candidate --mode replay
750
+ evalcore pairwise --suite suite.yaml --a baseline --b candidate --mode replay
691
751
  ```
692
752
 
693
753
  `sweep` prints a ranked leaderboard plus a full metric × variant matrix (a
694
- model × prompt-version grid is just several named variants) — reusing the
754
+ model × prompt-version grid is just several named variants) - reusing the
695
755
  per-variant runner unchanged; it's pure orchestration + tabulation.
696
756
 
697
757
  `pairwise` is the sharper subjective signal: instead of scoring each output
698
758
  in isolation, a judge is shown **both** variants' outputs for the same case
699
- and picks a winner, and evalkit reports A's win-rate (ties count half).
700
- Order is **counterbalanced** — each pair is judged both ways and a pick that
759
+ and picks a winner, and evalcore reports A's win-rate (ties count half).
760
+ Order is **counterbalanced** - each pair is judged both ways and a pick that
701
761
  flips when you swap the order collapses to a tie, so position bias can't
702
762
  manufacture a winner. Configure it under `thresholds.pairwise` (`content_ref`,
703
763
  `model`/`replay_path`, optional `rubric`/`context_refs`); it runs live
@@ -738,16 +798,16 @@ Built-ins: `http` + `replay` adapters; `classification`, `max_chars`,
738
798
  `regex_absent`, `regex_present`, `non_empty`, and `llm_judge` graders; and
739
799
  `markdown` + `html` reporters (single scorecards and comparative
740
800
  comparisons; pick one with `--report`). Register more with
741
- `evalkit.adapters.base.register` / `evalkit.graders.base.register` /
742
- `evalkit.reporters.base.register` and load them with `--plugins your.module`
801
+ `evalcore.adapters.base.register` / `evalcore.graders.base.register` /
802
+ `evalcore.reporters.base.register` and load them with `--plugins your.module`
743
803
  (CLI) or a plain import (Python API). If onboarding a new consumer ever
744
- requires touching `src/evalkit/`, that's an abstraction leak — fix the engine
804
+ requires touching `src/evalcore/`, that's an abstraction leak - fix the engine
745
805
  seam, don't fork it.
746
806
 
747
807
  ## Layout
748
808
 
749
809
  ```
750
- src/evalkit/
810
+ src/evalcore/
751
811
  models.py Case, Variant, Output, Score, Scorecard, Comparison (opaque-blob based)
752
812
  refs.py $ref resolution (the only thing that opens a consumer's blobs)
753
813
  loader.py suite + dataset loading, content hashes (YAML/JSON; suite-relative paths)
@@ -795,9 +855,9 @@ Known gaps / next:
795
855
  ## Releasing
796
856
 
797
857
  Releases publish to PyPI as **`evalcore`** via
798
- [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC) — no API
858
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC) - no API
799
859
  tokens are stored. One-time setup on PyPI: add a *pending publisher* for project
800
860
  `evalcore` pointing at owner `scottpmiller`, repo `evalcore`, workflow
801
861
  `publish.yml`, environment `pypi`. Then to cut a release: bump `version` in
802
- `pyproject.toml`, tag it, and publish a GitHub Release — `.github/workflows/publish.yml`
862
+ `pyproject.toml`, tag it, and publish a GitHub Release - `.github/workflows/publish.yml`
803
863
  builds the sdist + wheel and uploads them. (Point it at TestPyPI first for a dry run.)