evalcore 2.2.0__tar.gz → 2.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 (76) hide show
  1. {evalcore-2.2.0 → evalcore-2.3.0}/CHANGELOG.md +26 -1
  2. {evalcore-2.2.0 → evalcore-2.3.0}/PKG-INFO +15 -14
  3. {evalcore-2.2.0 → evalcore-2.3.0}/README.md +14 -13
  4. {evalcore-2.2.0 → evalcore-2.3.0}/pyproject.toml +1 -1
  5. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/graders/base.py +4 -0
  6. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/models.py +28 -0
  7. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/runner.py +30 -0
  8. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/store.py +16 -4
  9. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_store.py +56 -0
  10. {evalcore-2.2.0 → evalcore-2.3.0}/uv.lock +1 -1
  11. {evalcore-2.2.0 → evalcore-2.3.0}/.github/workflows/ci.yml +0 -0
  12. {evalcore-2.2.0 → evalcore-2.3.0}/.github/workflows/publish.yml +0 -0
  13. {evalcore-2.2.0 → evalcore-2.3.0}/.gitignore +0 -0
  14. {evalcore-2.2.0 → evalcore-2.3.0}/.pre-commit-config.yaml +0 -0
  15. {evalcore-2.2.0 → evalcore-2.3.0}/LICENSE +0 -0
  16. {evalcore-2.2.0 → evalcore-2.3.0}/docs/design.md +0 -0
  17. {evalcore-2.2.0 → evalcore-2.3.0}/examples/__init__.py +0 -0
  18. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/README.md +0 -0
  19. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/__init__.py +0 -0
  20. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/adapter.py +0 -0
  21. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/datasets/support_reply/v1/cases/angry_complaint.yaml +0 -0
  22. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/datasets/support_reply/v1/cases/billing_question.yaml +0 -0
  23. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/datasets/support_reply/v1/cases/double_charge.yaml +0 -0
  24. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/datasets/support_reply/v1/cases/refund_request.yaml +0 -0
  25. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/fixtures/support_reply_judge.yaml +0 -0
  26. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/fixtures/support_reply_pairwise.yaml +0 -0
  27. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/fixtures/support_reply_replay.yaml +0 -0
  28. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/graders.py +0 -0
  29. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/run_eval.py +0 -0
  30. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/suite.yaml +0 -0
  31. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/tests/__init__.py +0 -0
  32. {evalcore-2.2.0 → evalcore-2.3.0}/examples/quickstart/tests/test_quickstart.py +0 -0
  33. {evalcore-2.2.0 → evalcore-2.3.0}/justfile +0 -0
  34. {evalcore-2.2.0 → evalcore-2.3.0}/pyrightconfig.json +0 -0
  35. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/__init__.py +0 -0
  36. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/adapters/__init__.py +0 -0
  37. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/adapters/base.py +0 -0
  38. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/adapters/env.py +0 -0
  39. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/adapters/http.py +0 -0
  40. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/adapters/replay.py +0 -0
  41. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/cli.py +0 -0
  42. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/compare.py +0 -0
  43. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/errors.py +0 -0
  44. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/graders/__init__.py +0 -0
  45. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/graders/classification.py +0 -0
  46. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/graders/deterministic.py +0 -0
  47. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/graders/judge.py +0 -0
  48. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/graders/numeric.py +0 -0
  49. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/loader.py +0 -0
  50. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/pairwise.py +0 -0
  51. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/py.typed +0 -0
  52. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/rating.py +0 -0
  53. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/refs.py +0 -0
  54. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/report.py +0 -0
  55. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/reporters/__init__.py +0 -0
  56. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/reporters/base.py +0 -0
  57. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/reporters/html.py +0 -0
  58. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/reporters/markdown.py +0 -0
  59. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/retry.py +0 -0
  60. {evalcore-2.2.0 → evalcore-2.3.0}/src/evalcore/sweep.py +0 -0
  61. {evalcore-2.2.0 → evalcore-2.3.0}/tests/__init__.py +0 -0
  62. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_adapters.py +0 -0
  63. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_cli.py +0 -0
  64. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_edge_cases.py +0 -0
  65. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_judge.py +0 -0
  66. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_judge_extra.py +0 -0
  67. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_live_clients.py +0 -0
  68. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_pairwise_extra.py +0 -0
  69. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_rating.py +0 -0
  70. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_rating_server.py +0 -0
  71. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_reporters.py +0 -0
  72. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_retry.py +0 -0
  73. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_runner.py +0 -0
  74. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_sweep_pairwise.py +0 -0
  75. {evalcore-2.2.0 → evalcore-2.3.0}/tests/test_unit.py +0 -0
  76. {evalcore-2.2.0 → evalcore-2.3.0}/uv.toml +0 -0
@@ -6,6 +6,30 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.3.0] - 2026-08-08
10
+
11
+ A run now describes its own scores.
12
+
13
+ ### Added
14
+ - `RunResult.graders`, a map of grader name to `models.GraderInfo`
15
+ (`category`, `scale`), filled in by the runner from the graders it built.
16
+ A `Score` names the grader that emitted it and nothing else, so a run could
17
+ not previously answer for itself: a `run.json` written by one CI step and
18
+ published by a later one needed the suite file alongside it, and without one
19
+ every grader reported `unknown` and a suite with a judge could not be
20
+ published at all. Run-grain rather than score-grain - one entry per grader,
21
+ not two fields repeated across a couple of hundred rows.
22
+ - `models.GraderInfo`.
23
+ - `register` also records the category on the decorated class as
24
+ `grader_category`, so a grader instance can answer for itself. The runner
25
+ reads it the same way it already collects `judge_version`.
26
+
27
+ ### Changed
28
+ - `store.score_rows` takes the grader category and judge scale from
29
+ `run.graders`. `grader_types` and `judge_scales` still override it, and are
30
+ how a run written before this release - whose map is empty - publishes
31
+ correctly. `grader_lookups` is unchanged and still builds them from a suite.
32
+
9
33
  ## [2.2.0] - 2026-08-08
10
34
 
11
35
  ### Added
@@ -196,7 +220,8 @@ by semantic versioning: a breaking change to either means a 2.0.
196
220
  rating + ranking with judge agreement, Markdown/HTML reporters, JSON +
197
221
  column-store outbox, and content-hash provenance.
198
222
 
199
- [Unreleased]: https://github.com/scottpmiller/evalcore/compare/2.2.0...HEAD
223
+ [Unreleased]: https://github.com/scottpmiller/evalcore/compare/2.3.0...HEAD
224
+ [2.3.0]: https://github.com/scottpmiller/evalcore/compare/2.2.0...2.3.0
200
225
  [2.2.0]: https://github.com/scottpmiller/evalcore/compare/2.1.0...2.2.0
201
226
  [2.1.0]: https://github.com/scottpmiller/evalcore/compare/2.0.0...2.1.0
202
227
  [2.0.0]: https://github.com/scottpmiller/evalcore/compare/1.0.0...2.0.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evalcore
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs.
5
5
  Project-URL: Homepage, https://github.com/scottpmiller/evalcore
6
6
  Project-URL: Repository, https://github.com/scottpmiller/evalcore
@@ -513,19 +513,12 @@ print(report.render_comparison(result))
513
513
  store.write_scorecard('candidate.scorecard.json', candidate.scorecard)
514
514
  store.write_comparison('comparison.json', result)
515
515
 
516
- # The rows. grader_lookups supplies the grader category and judge scale a
517
- # Score doesn't carry; the baseline half exports without the comparison,
518
- # since it was not itself gated.
519
- types, scales = store.grader_lookups(suite.graders)
516
+ # The rows. A run carries what its graders are, so nothing else is needed.
517
+ # The baseline half exports without the comparison, since it was not itself
518
+ # gated.
520
519
  exporter = store.JsonlOutboxExporter('outbox.jsonl')
521
- exporter.export_scores(baseline, grader_types=types, judge_scales=scales)
522
- exporter.export_scores(
523
- candidate,
524
- result,
525
- baseline_run_id=baseline.run_id,
526
- grader_types=types,
527
- judge_scales=scales,
528
- )
520
+ exporter.export_scores(baseline)
521
+ exporter.export_scores(candidate, result, baseline_run_id=baseline.run_id)
529
522
 
530
523
  raise SystemExit(0 if result.verdict != 'fail' else 1)
531
524
  ```
@@ -811,9 +804,17 @@ constructor line, so an offline run and a live one share a code path:
811
804
  ```python
812
805
  exporter = store.JsonlOutboxExporter(path) # offline
813
806
  exporter = KafkaOutboxExporter(...) # live, from another package
814
- exporter.export_scores(run, comparison, grader_types=..., judge_scales=...)
807
+ exporter.export_scores(run, comparison)
815
808
  ```
816
809
 
810
+ A `RunResult` carries `graders`, a map of grader name to its category and
811
+ judge scale, so it describes its own scores and an exporter needs nothing but
812
+ the run. That matters most for an artifact: a `run.json` written by one CI
813
+ step and published by a later one used to need the suite file alongside it,
814
+ and without one every grader reported `unknown`. `grader_types` and
815
+ `judge_scales` still override the map, which is how a run written before
816
+ 2.3.0 publishes correctly - `grader_lookups(suite.graders)` builds them.
817
+
817
818
  ---
818
819
 
819
820
  ## The two extension seams (recap)
@@ -481,19 +481,12 @@ print(report.render_comparison(result))
481
481
  store.write_scorecard('candidate.scorecard.json', candidate.scorecard)
482
482
  store.write_comparison('comparison.json', result)
483
483
 
484
- # The rows. grader_lookups supplies the grader category and judge scale a
485
- # Score doesn't carry; the baseline half exports without the comparison,
486
- # since it was not itself gated.
487
- types, scales = store.grader_lookups(suite.graders)
484
+ # The rows. A run carries what its graders are, so nothing else is needed.
485
+ # The baseline half exports without the comparison, since it was not itself
486
+ # gated.
488
487
  exporter = store.JsonlOutboxExporter('outbox.jsonl')
489
- exporter.export_scores(baseline, grader_types=types, judge_scales=scales)
490
- exporter.export_scores(
491
- candidate,
492
- result,
493
- baseline_run_id=baseline.run_id,
494
- grader_types=types,
495
- judge_scales=scales,
496
- )
488
+ exporter.export_scores(baseline)
489
+ exporter.export_scores(candidate, result, baseline_run_id=baseline.run_id)
497
490
 
498
491
  raise SystemExit(0 if result.verdict != 'fail' else 1)
499
492
  ```
@@ -779,9 +772,17 @@ constructor line, so an offline run and a live one share a code path:
779
772
  ```python
780
773
  exporter = store.JsonlOutboxExporter(path) # offline
781
774
  exporter = KafkaOutboxExporter(...) # live, from another package
782
- exporter.export_scores(run, comparison, grader_types=..., judge_scales=...)
775
+ exporter.export_scores(run, comparison)
783
776
  ```
784
777
 
778
+ A `RunResult` carries `graders`, a map of grader name to its category and
779
+ judge scale, so it describes its own scores and an exporter needs nothing but
780
+ the run. That matters most for an artifact: a `run.json` written by one CI
781
+ step and published by a later one used to need the suite file alongside it,
782
+ and without one every grader reported `unknown`. `grader_types` and
783
+ `judge_scales` still override the map, which is how a run written before
784
+ 2.3.0 publishes correctly - `grader_lookups(suite.graders)` builds them.
785
+
785
786
  ---
786
787
 
787
788
  ## The two extension seams (recap)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "evalcore"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  description = "A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -94,6 +94,10 @@ def register(
94
94
  raise ConfigError(f'grader type {type_name!r} already registered')
95
95
  _REGISTRY[type_name] = cls
96
96
  _CATEGORIES[type_name] = GraderType(category)
97
+ # Also on the class, so an instance can answer for itself. The runner
98
+ # reads it off the graders it built to make the run self-describing,
99
+ # the same way it collects `judge_version`.
100
+ cls.grader_category = GraderType(category)
97
101
  return cls
98
102
 
99
103
  return _decorate
@@ -162,6 +162,28 @@ class Scorecard(pydantic.BaseModel):
162
162
  metrics: dict[str, MetricValue] = pydantic.Field(default_factory=dict)
163
163
 
164
164
 
165
+ class GraderInfo(pydantic.BaseModel):
166
+ """What a grader is, as opposed to what it scored.
167
+
168
+ A ``Score`` names the grader that emitted it and nothing else, so a run
169
+ that did not carry this could not describe its own scores: publishing it
170
+ needed the suite file alongside it, and a ``run.json`` handed to a later
171
+ CI step without one reported every grader as ``unknown``.
172
+
173
+ Run-grain, not score-grain: one entry per grader rather than the same two
174
+ fields repeated on a couple of hundred rows.
175
+ """
176
+
177
+ #: The registered category, a ``graders.GraderType`` value. A plain `str`
178
+ #: here because `graders.base` imports this module, so this module cannot
179
+ #: import it back; ``GraderType`` is a ``StrEnum`` and compares equal.
180
+ category: str = 'unknown'
181
+ #: The scale raw judge points sit on, 0 for a grader that is not a judge.
182
+ #: Normalization is points / scale, so the points are unreadable without
183
+ #: it.
184
+ scale: int = 0
185
+
186
+
165
187
  class RunResult(pydantic.BaseModel):
166
188
  """Everything one run produced: the scorecard plus per-sample results.
167
189
 
@@ -174,12 +196,18 @@ class RunResult(pydantic.BaseModel):
174
196
  over the whole run. The scorecard keeps their values but not which grader
175
197
  emitted them or what it reported, so they are retained here too - a
176
198
  results-store row needs the grader attribution.
199
+
200
+ ``graders`` maps grader name to what that grader is, so the run is
201
+ self-describing: an exporter needs nothing but the run. Empty on runs
202
+ written before 2.3.0, which is why exporters still accept the lookups as
203
+ a fallback.
177
204
  """
178
205
 
179
206
  run_id: str
180
207
  scorecard: Scorecard
181
208
  results: list[CaseResult] = pydantic.Field(default_factory=list)
182
209
  aggregate_scores: list[Score] = pydantic.Field(default_factory=list)
210
+ graders: dict[str, GraderInfo] = pydantic.Field(default_factory=dict)
183
211
 
184
212
 
185
213
  class Rating(pydantic.BaseModel):
@@ -84,6 +84,35 @@ def _uuid7() -> uuid.UUID:
84
84
  )
85
85
 
86
86
 
87
+ def _grader_info(*grader_groups) -> dict[str, models.GraderInfo]:
88
+ """What each grader is, so the run can describe its own scores.
89
+
90
+ Read off the grader instances rather than the suite config, so a run is
91
+ publishable from the artifact alone - no suite file alongside it. Both
92
+ attributes are duck-typed the way ``judge_version`` is: ``register`` puts
93
+ ``grader_category`` on the class, and only a judge has a ``scale``.
94
+
95
+ Args:
96
+ grader_groups: The per-case and aggregate grader lists.
97
+
98
+ Returns:
99
+ Grader name to its category and judge scale.
100
+
101
+ """
102
+ return {
103
+ grader.name: models.GraderInfo(
104
+ category=str(
105
+ getattr(
106
+ grader, 'grader_category', graders_base.GraderType.UNKNOWN
107
+ )
108
+ ),
109
+ scale=int(getattr(grader, 'scale', 0) or 0),
110
+ )
111
+ for group in grader_groups
112
+ for grader in group
113
+ }
114
+
115
+
87
116
  def _aggregate_metrics(
88
117
  results: list[models.CaseResult], agg_scores: list[models.Score]
89
118
  ) -> dict[str, models.MetricValue]:
@@ -321,6 +350,7 @@ async def run_suite(
321
350
  scorecard=scorecard,
322
351
  results=results,
323
352
  aggregate_scores=agg_scores,
353
+ graders=_grader_info(per_case_graders, aggregate_graders),
324
354
  )
325
355
 
326
356
 
@@ -402,8 +402,12 @@ def score_rows(
402
402
 
403
403
  ``comparison`` stamps the run-grain gate columns and the per-metric ``win``
404
404
  and ``guardrail`` fields; without it the run reads as ungated.
405
- ``grader_types`` and ``judge_scales`` are grader-name lookups the caller
406
- builds from the suite, since a ``Score`` carries neither.
405
+ The grader category and judge scale come from ``run.graders``, which the
406
+ runner fills in - a ``Score`` carries neither, and a run that could not
407
+ answer for itself was not publishable from its own artifact.
408
+ ``grader_types`` and ``judge_scales`` override it, and are how a run
409
+ written before 2.3.0 (whose map is empty) still publishes correctly;
410
+ :func:`grader_lookups` builds them from the suite.
407
411
  """
408
412
  key = _run_key(run.scorecard)
409
413
  gate = _gate(comparison, baseline_run_id)
@@ -412,8 +416,16 @@ def score_rows(
412
416
  if comparison
413
417
  else {}
414
418
  )
415
- types = grader_types or {}
416
- scales = judge_scales or {}
419
+ # The run describes its own graders as of 2.3.0. The lookups remain for
420
+ # runs written before that, which carry an empty map - a `run.json` on
421
+ # disk outlives the release that wrote it. An explicit lookup still wins,
422
+ # so a caller can correct a run it did not produce.
423
+ types = {name: info.category for name, info in run.graders.items()}
424
+ scales = {
425
+ name: info.scale for name, info in run.graders.items() if info.scale
426
+ }
427
+ types.update(grader_types or {})
428
+ scales.update(judge_scales or {})
417
429
  rows: list[dict] = []
418
430
  scored: set[str] = set()
419
431
 
@@ -338,6 +338,62 @@ class GraderLookupTests(unittest.TestCase):
338
338
  name = 'non_empty'
339
339
 
340
340
 
341
+ class SelfDescribingRunTests(unittest.TestCase):
342
+ """A run carries what its scores mean, so an artifact is publishable."""
343
+
344
+ @staticmethod
345
+ def _run_with_graders():
346
+ run = _run()
347
+ run.graders = {
348
+ 'det': models.GraderInfo(category='heuristic'),
349
+ 'j': models.GraderInfo(category='llm_as_judge', scale=7),
350
+ 'cls': models.GraderInfo(category='statistical'),
351
+ }
352
+ return run
353
+
354
+ def test_the_run_supplies_the_lookups(self):
355
+ rows = store.score_rows(self._run_with_graders())
356
+ types = {row['grader']: row['grader_type'] for row in rows}
357
+ self.assertEqual(types['det'], 'heuristic')
358
+ self.assertEqual(types['j'], 'llm_as_judge')
359
+
360
+ def test_a_judge_scale_comes_off_the_run(self):
361
+ rows = store.score_rows(self._run_with_graders())
362
+ judged = {r['judge_scale'] for r in rows if r['grader'] == 'j'}
363
+ plain = {r['judge_scale'] for r in rows if r['grader'] == 'det'}
364
+ self.assertEqual(judged, {7})
365
+ self.assertEqual(plain, {0})
366
+
367
+ def test_an_explicit_lookup_still_wins(self):
368
+ """So a caller can correct a run it did not produce."""
369
+ rows = store.score_rows(
370
+ self._run_with_graders(), grader_types={'det': 'statistical'}
371
+ )
372
+ types = {row['grader']: row['grader_type'] for row in rows}
373
+ self.assertEqual(types['det'], 'statistical')
374
+
375
+ def test_a_run_from_before_the_field_still_publishes(self):
376
+ """The map is empty on runs written by older versions, so the
377
+ lookups have to stay."""
378
+ run = _run()
379
+ self.assertEqual(run.graders, {})
380
+ rows = store.score_rows(run, grader_types={'det': 'heuristic'})
381
+ types = {row['grader']: row['grader_type'] for row in rows}
382
+ self.assertEqual(types['det'], 'heuristic')
383
+
384
+ def test_it_survives_the_json_round_trip(self):
385
+ """The whole point: no suite file alongside the artifact."""
386
+ with tempfile.TemporaryDirectory() as tmp:
387
+ path = pathlib.Path(tmp) / 'run.json'
388
+ store.write_run(path, self._run_with_graders())
389
+ reloaded = store.read_run(path)
390
+ self.assertEqual(reloaded.graders['j'].scale, 7)
391
+ rows = store.score_rows(reloaded)
392
+ types = {row['grader']: row['grader_type'] for row in rows}
393
+ self.assertEqual(types['det'], 'heuristic')
394
+ self.assertEqual(types['j'], 'llm_as_judge')
395
+
396
+
341
397
  class ScoreExporterProtocolTests(unittest.TestCase):
342
398
  """The seam another package implements to publish to a real store."""
343
399
 
@@ -188,7 +188,7 @@ wheels = [
188
188
 
189
189
  [[package]]
190
190
  name = "evalcore"
191
- version = "2.2.0"
191
+ version = "2.3.0"
192
192
  source = { editable = "." }
193
193
  dependencies = [
194
194
  { name = "pydantic" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes