evalseal 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. evalseal-0.1.0/.env.example +4 -0
  2. evalseal-0.1.0/.github/workflows/ci.yml +33 -0
  3. evalseal-0.1.0/.github/workflows/release.yml +52 -0
  4. evalseal-0.1.0/.gitignore +223 -0
  5. evalseal-0.1.0/DESIGN.md +76 -0
  6. evalseal-0.1.0/LICENSE +21 -0
  7. evalseal-0.1.0/PKG-INFO +116 -0
  8. evalseal-0.1.0/README.md +86 -0
  9. evalseal-0.1.0/examples/borderline_judge/dataset.jsonl +20 -0
  10. evalseal-0.1.0/examples/borderline_judge/scorer.json +6 -0
  11. evalseal-0.1.0/examples/borderline_judge/target.json +1 -0
  12. evalseal-0.1.0/pyproject.toml +48 -0
  13. evalseal-0.1.0/src/evalseal/__init__.py +3 -0
  14. evalseal-0.1.0/src/evalseal/adapters/__init__.py +0 -0
  15. evalseal-0.1.0/src/evalseal/adapters/dataset.py +36 -0
  16. evalseal-0.1.0/src/evalseal/adapters/recording.py +56 -0
  17. evalseal-0.1.0/src/evalseal/adapters/scorer.py +74 -0
  18. evalseal-0.1.0/src/evalseal/adapters/target.py +97 -0
  19. evalseal-0.1.0/src/evalseal/analyze.py +82 -0
  20. evalseal-0.1.0/src/evalseal/cli.py +152 -0
  21. evalseal-0.1.0/src/evalseal/executor.py +142 -0
  22. evalseal-0.1.0/src/evalseal/ledger.py +60 -0
  23. evalseal-0.1.0/src/evalseal/models.py +78 -0
  24. evalseal-0.1.0/src/evalseal/report.py +48 -0
  25. evalseal-0.1.0/tests/cassettes/run.json +4424 -0
  26. evalseal-0.1.0/tests/conftest.py +25 -0
  27. evalseal-0.1.0/tests/test_analyze.py +45 -0
  28. evalseal-0.1.0/tests/test_cli.py +124 -0
  29. evalseal-0.1.0/tests/test_executor.py +53 -0
  30. evalseal-0.1.0/tests/test_ledger.py +92 -0
  31. evalseal-0.1.0/tests/test_provenance_gap.py +84 -0
  32. evalseal-0.1.0/tests/test_recording.py +43 -0
  33. evalseal-0.1.0/tests/test_verify.py +75 -0
@@ -0,0 +1,4 @@
1
+ # Copy to .env and fill in. .env is gitignored — never commit a real key.
2
+ # Free Gemini key: https://aistudio.google.com/apikey
3
+ # Only used when recording (EVALSEAL_RECORD=1); replay and CI need no key.
4
+ EVALSEAL_API_KEY=
@@ -0,0 +1,33 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ python-version: ["3.11", "3.12"]
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with: { python-version: "${{ matrix.python-version }}", cache: pip }
13
+ - run: pip install -e ".[dev]"
14
+ - name: Tests (no API key — cassettes replay)
15
+ run: pytest -q --cov=evalseal
16
+ - name: Demo run reproduces from cassette
17
+ run: |
18
+ if [ ! -f tests/cassettes/run.json ]; then
19
+ echo "::warning::tests/cassettes/run.json not recorded yet; skipping demo replay."
20
+ exit 0
21
+ fi
22
+ set +e
23
+ evalseal run \
24
+ --dataset examples/borderline_judge/dataset.jsonl \
25
+ --target-config examples/borderline_judge/target.json \
26
+ --scorer-config examples/borderline_judge/scorer.json \
27
+ --n 5 --cassette tests/cassettes/run.json
28
+ code=$?
29
+ # 0 = all stable, 3 = unstable cases found (expected for this demo).
30
+ # Anything else means the replay itself broke.
31
+ if [ "$code" -ne 0 ] && [ "$code" -ne 3 ]; then exit "$code"; fi
32
+ - name: Ledger integrity
33
+ run: evalseal verify
@@ -0,0 +1,52 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with: { python-version: "3.12", cache: pip }
13
+ - name: Tag matches package version
14
+ run: |
15
+ version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
16
+ if [ "${GITHUB_REF_NAME#v}" != "$version" ]; then
17
+ echo "::error::tag $GITHUB_REF_NAME does not match pyproject version $version"; exit 1
18
+ fi
19
+ - run: pip install -e ".[dev]" build twine
20
+ - name: Tests (no API key — cassettes replay)
21
+ run: pytest -q
22
+ - run: python -m build
23
+ - run: twine check dist/*
24
+ - uses: actions/upload-artifact@v4
25
+ with: { name: dist, path: dist/ }
26
+
27
+ publish-pypi:
28
+ needs: build
29
+ runs-on: ubuntu-latest
30
+ # Trusted publishing: PyPI verifies this workflow via OIDC. No token is stored.
31
+ environment:
32
+ name: pypi
33
+ url: https://pypi.org/project/evalseal/
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with: { name: dist, path: dist/ }
39
+ - uses: pypa/gh-action-pypi-publish@release/v1
40
+
41
+ github-release:
42
+ needs: publish-pypi
43
+ runs-on: ubuntu-latest
44
+ permissions:
45
+ contents: write
46
+ steps:
47
+ - uses: actions/download-artifact@v4
48
+ with: { name: dist, path: dist/ }
49
+ - name: Create GitHub release
50
+ env:
51
+ GH_TOKEN: ${{ github.token }}
52
+ run: gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --title "$GITHUB_REF_NAME" --generate-notes
@@ -0,0 +1,223 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+
220
+ # EvalSeal local state and generated reports
221
+ .evalseal/
222
+ report.json
223
+ report.md
@@ -0,0 +1,76 @@
1
+ # EvalSeal design
2
+
3
+ ## What it claims, and what it doesn't
4
+
5
+ **EvalSeal measures reproducibility. It does not create determinism.** Setting a
6
+ temperature or seed does not make a hosted model deterministic, and EvalSeal never says it
7
+ does. It runs every case N times and reports how often the verdict disagrees with itself.
8
+
9
+ **A green CI replay proves the harness is reproducible, not that the model is.** Replaying
10
+ a cassette gives back the recorded responses, so the analysis, scores and report can be
11
+ regenerated from them exactly. It says nothing about whether the live model would answer
12
+ the same way today. To learn that, record again and `diff` the runs.
13
+
14
+ **The ledger is tamper-evident, not tamper-proof.** Each record's SHA-256 covers its whole
15
+ content plus the previous record's hash. Editing a past score breaks that record's hash,
16
+ and re-hashing it breaks the next record's link. Anyone who can rewrite the *whole* file
17
+ can build a new valid chain, though. Anchoring the head hash somewhere external
18
+ (signing, notarization) is out of scope for v0.
19
+
20
+ ## Components
21
+
22
+ | module | role |
23
+ |---|---|
24
+ | `analyze.py` | Pure functions: seeded bootstrap CI, flip rate, stability class. No I/O. |
25
+ | `adapters/recording.py` | Cassette keyed by the SHA-256 of the effective request body. |
26
+ | `adapters/target.py` | `Target` protocol; `LocalCallableTarget` (tests), `OpenAICompatibleTarget`. |
27
+ | `adapters/scorer.py` | Exact, regex, and LLM-judge scorers. The judge is a `Target`. |
28
+ | `executor.py` | N-run loop, provenance capture, provenance-gap warnings. |
29
+ | `ledger.py` | Hash-linked append-only JSONL; `verify_chain`. |
30
+ | `report.py`, `cli.py` | `report.md` / `report.json`; `run`, `verify`, `diff`. |
31
+
32
+ ## Decisions
33
+
34
+ **The cassette stores responses as ordered lists.** Repeats of a case send identical
35
+ requests. If the cassette kept one response per request hash, repeat #1 would be replayed
36
+ N times and every recorded flip would disappear. Instead, the k-th identical call gets the
37
+ k-th recorded response. This depends on calls happening in the same order in record and
38
+ replay mode, which holds because the executor is sequential.
39
+
40
+ **Only the effective request is hashed.** The cassette key is the URL plus the JSON body
41
+ actually sent. Unset parameters are left out of the body entirely, so "temperature not
42
+ set" and "temperature=1.0" are different requests. API keys never enter the key or the
43
+ cassette.
44
+
45
+ **Provenance is checked on every response.** A served-model mismatch on call 37 matters
46
+ even when call 100 looks clean. The manifest records the first response's metadata, and
47
+ drift warnings flag any change in served model or system fingerprint during the run.
48
+
49
+ **Dated snapshots are not mismatches.** Requesting `gpt-4o-mini` and being served
50
+ `gpt-4o-mini-2024-07-18` is the provider resolving an alias. The snapshot is still stored
51
+ in `served_model`. Any other difference produces a warning.
52
+
53
+ **Judge verdict parsing is strict.** The first standalone `PASS`/`FAIL` token decides, and
54
+ anything else counts as FAIL. A plain substring check would read "FAIL — does not PASS" as
55
+ a pass.
56
+
57
+ **Exit codes separate outcomes.** `run` exits 3 when any case is UNSTABLE, which is
58
+ different from 1 (error) and 2 (usage). CI can then accept an expected unstable demo while
59
+ still failing on a broken replay.
60
+
61
+ ## Known limits
62
+
63
+ - **Small-N bootstrap CIs are approximate.** With N=5 binary verdicts the bootstrap
64
+ distribution is coarse (steps of 0.2) and tends to under-cover. The CI's seed is fixed,
65
+ so the interval itself is reproducible, but it is still only an approximation.
66
+ - **Flip rate needs N ≥ 5 to mean much.** At N=3 a single dissent already reads as 33%.
67
+ Use a larger N on the cases you care about.
68
+ - **Stability thresholds are conventions.** `BORDERLINE_MAX_FLIP = 0.20` is a named
69
+ constant chosen for auditability, not a statistically derived cutoff.
70
+ - **The `diff` noise floor is conservative.** It uses the widest per-case CI half-width
71
+ across both runs. That rarely claims a false "REAL CHANGE", but it will miss small real
72
+ shifts in the aggregate. A paired test across cases would be more powerful.
73
+ - **Float scores use a median-crossing pseudo-flip.** This gives a variance signal
74
+ without a threshold, but it is a heuristic. All built-in scorers are binary.
75
+ - **Canonical hosts are an allowlist.** Any self-hosted or gateway endpoint gets a
76
+ NON-CANONICAL warning by design. The warning means "provenance unverified", not "wrong".
evalseal-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Venkata Manideep Patibandla
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.5
2
+ Name: evalseal
3
+ Version: 0.1.0
4
+ Summary: Reproducibility and provenance receipts for LLM evaluations.
5
+ Project-URL: Homepage, https://github.com/patibandlavenkatamanideep/evalseal
6
+ Project-URL: Issues, https://github.com/patibandlavenkatamanideep/evalseal/issues
7
+ Author: Venkata Manideep Patibandla
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: evaluation,llm,llm-as-judge,provenance,reproducibility
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
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: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: pydantic>=2.6
24
+ Requires-Dist: rich>=13.7
25
+ Requires-Dist: typer>=0.12
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # EvalSeal
32
+
33
+ **Reproducibility receipts for LLM evals: run it N times, report the score with its noise, and seal what actually ran.**
34
+
35
+ ## The problem
36
+
37
+ An eval score from a single run is one sample of a random process. Run the same eval again
38
+ and borderline items quietly flip from PASS to FAIL, especially when an LLM judge grades
39
+ them. Reports also name the model you *asked* for, not the one that *answered*. EvalSeal
40
+ measures the flips, records the real provenance, and seals both into a tamper-evident ledger.
41
+
42
+ ## Quickstart
43
+
44
+ ```bash
45
+ pip install evalseal
46
+
47
+ # The recorded demo (examples + cassette) lives in the repo.
48
+ git clone https://github.com/patibandlavenkatamanideep/evalseal && cd evalseal
49
+
50
+ # Replays the committed cassette: no API key, no network.
51
+ evalseal run \
52
+ --dataset examples/borderline_judge/dataset.jsonl \
53
+ --target-config examples/borderline_judge/target.json \
54
+ --scorer-config examples/borderline_judge/scorer.json \
55
+ --n 5
56
+ ```
57
+
58
+ ## Real flip rates
59
+
60
+ Recorded 2026-09-15 and committed in `tests/cassettes/run.json`: `gemini-2.5-flash` as both
61
+ the target and the judge, temperature left at the provider default, 20 arguable prompts,
62
+ 5 runs each. The quickstart above replays exactly this run.
63
+
64
+ **Mean score: 0.92, but 5 of 20 cases did not get the same verdict every time.**
65
+
66
+ | case | prompt | verdicts | mean | 95% CI | flip rate | stability |
67
+ |---|---|---|---|---|---|---|
68
+ | b01 | Is a hot dog a sandwich? | `FPPFP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
69
+ | b10 | Blockchain for a child in exactly 20 words | `FPFPP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
70
+ | b16 | "Do we only use 10% of our brains?" in a jokey tone | `PPFFP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
71
+ | b05 | A borderline-polite refusal to a coworker | `PFPPP` | 0.80 | [0.40, 1.00] | 20% | BORDERLINE |
72
+ | b19 | A technically accurate haiku about recursion | `PPFPP` | 0.80 | [0.40, 1.00] | 20% | BORDERLINE |
73
+ | 15 others | | `PPPPP` | 1.00 | [1.00, 1.00] | 0% | STABLE |
74
+
75
+ Treat the k-th repeat of every case as one ordinary single-run eval, and the five
76
+ "single runs" of this identical eval scored **0.90, 0.95, 0.85, 0.90 and 1.00**. A single
77
+ run can't tell you which of those numbers you got.
78
+
79
+ The report also flagged `TEMPERATURE NOT SET` for both the target and the judge, which is
80
+ the reason these borderline verdicts can come out differently from run to run.
81
+
82
+ To re-record with your own key, copy `.env.example` to `.env`, add a free
83
+ [Google AI Studio](https://aistudio.google.com/apikey) key, and run the quickstart with
84
+ `EVALSEAL_RECORD=1`. If the free tier rate-limits you, run the same command again later;
85
+ responses already recorded are kept.
86
+
87
+ ## Commands
88
+
89
+ | command | what it does | exit code |
90
+ |---|---|---|
91
+ | `evalseal run` | Runs each case N times, analyzes variance, seals a record, writes `report.json` + `report.md`. | `0` all stable/borderline · `3` any case UNSTABLE · `1` error |
92
+ | `evalseal verify` | Recomputes every hash in `.evalseal/ledger.jsonl` and checks the chain links. | `0` intact · `1` tampered or broken |
93
+ | `evalseal diff A B` | Compares two ledger runs and says whether the mean moved beyond the noise floor. Use `--` for negative indices: `evalseal diff -- 0 -1`. | `0` |
94
+
95
+ **Stability classes** are based on the flip rate, the share of a case's N verdicts that
96
+ disagree with its majority: `STABLE` (0), `BORDERLINE` (≤ 20%), `UNSTABLE` (> 20%).
97
+
98
+ **Provenance warnings** show up in the report when:
99
+ - the served model differs from the requested one (for the target or the judge),
100
+ - the endpoint isn't a canonical provider host,
101
+ - temperature was left at the provider default,
102
+ - the served model or system fingerprint changed partway through the run.
103
+
104
+ ## How it works
105
+
106
+ The executor sends each prompt to the target N times and scores every response. An LLM
107
+ judge is itself a target, so its own randomness is measured instead of assumed away.
108
+ `analyze.py` computes the mean, a seeded bootstrap 95% CI, and the flip rate for each case.
109
+ Every request goes through a cassette. In record mode, real responses are saved in call
110
+ order; in replay mode, which is the default and what CI uses, they are served back, and a
111
+ missing entry fails loudly. Each run is saved as a `RunRecord`: its manifest (requested vs.
112
+ served model, fingerprint, parameters and whether they were set explicitly, rubric hash,
113
+ dataset hash) plus its results. The record is hashed and linked to the previous record's
114
+ hash in an append-only JSONL ledger, so editing any past score breaks `verify`.
115
+
116
+ See [DESIGN.md](https://github.com/patibandlavenkatamanideep/evalseal/blob/main/DESIGN.md) for what this does and does not prove.
@@ -0,0 +1,86 @@
1
+ # EvalSeal
2
+
3
+ **Reproducibility receipts for LLM evals: run it N times, report the score with its noise, and seal what actually ran.**
4
+
5
+ ## The problem
6
+
7
+ An eval score from a single run is one sample of a random process. Run the same eval again
8
+ and borderline items quietly flip from PASS to FAIL, especially when an LLM judge grades
9
+ them. Reports also name the model you *asked* for, not the one that *answered*. EvalSeal
10
+ measures the flips, records the real provenance, and seals both into a tamper-evident ledger.
11
+
12
+ ## Quickstart
13
+
14
+ ```bash
15
+ pip install evalseal
16
+
17
+ # The recorded demo (examples + cassette) lives in the repo.
18
+ git clone https://github.com/patibandlavenkatamanideep/evalseal && cd evalseal
19
+
20
+ # Replays the committed cassette: no API key, no network.
21
+ evalseal run \
22
+ --dataset examples/borderline_judge/dataset.jsonl \
23
+ --target-config examples/borderline_judge/target.json \
24
+ --scorer-config examples/borderline_judge/scorer.json \
25
+ --n 5
26
+ ```
27
+
28
+ ## Real flip rates
29
+
30
+ Recorded 2026-09-15 and committed in `tests/cassettes/run.json`: `gemini-2.5-flash` as both
31
+ the target and the judge, temperature left at the provider default, 20 arguable prompts,
32
+ 5 runs each. The quickstart above replays exactly this run.
33
+
34
+ **Mean score: 0.92, but 5 of 20 cases did not get the same verdict every time.**
35
+
36
+ | case | prompt | verdicts | mean | 95% CI | flip rate | stability |
37
+ |---|---|---|---|---|---|---|
38
+ | b01 | Is a hot dog a sandwich? | `FPPFP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
39
+ | b10 | Blockchain for a child in exactly 20 words | `FPFPP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
40
+ | b16 | "Do we only use 10% of our brains?" in a jokey tone | `PPFFP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
41
+ | b05 | A borderline-polite refusal to a coworker | `PFPPP` | 0.80 | [0.40, 1.00] | 20% | BORDERLINE |
42
+ | b19 | A technically accurate haiku about recursion | `PPFPP` | 0.80 | [0.40, 1.00] | 20% | BORDERLINE |
43
+ | 15 others | | `PPPPP` | 1.00 | [1.00, 1.00] | 0% | STABLE |
44
+
45
+ Treat the k-th repeat of every case as one ordinary single-run eval, and the five
46
+ "single runs" of this identical eval scored **0.90, 0.95, 0.85, 0.90 and 1.00**. A single
47
+ run can't tell you which of those numbers you got.
48
+
49
+ The report also flagged `TEMPERATURE NOT SET` for both the target and the judge, which is
50
+ the reason these borderline verdicts can come out differently from run to run.
51
+
52
+ To re-record with your own key, copy `.env.example` to `.env`, add a free
53
+ [Google AI Studio](https://aistudio.google.com/apikey) key, and run the quickstart with
54
+ `EVALSEAL_RECORD=1`. If the free tier rate-limits you, run the same command again later;
55
+ responses already recorded are kept.
56
+
57
+ ## Commands
58
+
59
+ | command | what it does | exit code |
60
+ |---|---|---|
61
+ | `evalseal run` | Runs each case N times, analyzes variance, seals a record, writes `report.json` + `report.md`. | `0` all stable/borderline · `3` any case UNSTABLE · `1` error |
62
+ | `evalseal verify` | Recomputes every hash in `.evalseal/ledger.jsonl` and checks the chain links. | `0` intact · `1` tampered or broken |
63
+ | `evalseal diff A B` | Compares two ledger runs and says whether the mean moved beyond the noise floor. Use `--` for negative indices: `evalseal diff -- 0 -1`. | `0` |
64
+
65
+ **Stability classes** are based on the flip rate, the share of a case's N verdicts that
66
+ disagree with its majority: `STABLE` (0), `BORDERLINE` (≤ 20%), `UNSTABLE` (> 20%).
67
+
68
+ **Provenance warnings** show up in the report when:
69
+ - the served model differs from the requested one (for the target or the judge),
70
+ - the endpoint isn't a canonical provider host,
71
+ - temperature was left at the provider default,
72
+ - the served model or system fingerprint changed partway through the run.
73
+
74
+ ## How it works
75
+
76
+ The executor sends each prompt to the target N times and scores every response. An LLM
77
+ judge is itself a target, so its own randomness is measured instead of assumed away.
78
+ `analyze.py` computes the mean, a seeded bootstrap 95% CI, and the flip rate for each case.
79
+ Every request goes through a cassette. In record mode, real responses are saved in call
80
+ order; in replay mode, which is the default and what CI uses, they are served back, and a
81
+ missing entry fails loudly. Each run is saved as a `RunRecord`: its manifest (requested vs.
82
+ served model, fingerprint, parameters and whether they were set explicitly, rubric hash,
83
+ dataset hash) plus its results. The record is hashed and linked to the previous record's
84
+ hash in an append-only JSONL ledger, so editing any past score breaks `verify`.
85
+
86
+ See [DESIGN.md](https://github.com/patibandlavenkatamanideep/evalseal/blob/main/DESIGN.md) for what this does and does not prove.
@@ -0,0 +1,20 @@
1
+ {"case_id": "b01", "prompt": "Is a hot dog a sandwich? Answer yes or no with one sentence.", "expected": null}
2
+ {"case_id": "b02", "prompt": "Summarize the causes of WWI in exactly one sentence.", "expected": null}
3
+ {"case_id": "b03", "prompt": "Give a 'mostly correct' explanation of how vaccines work in 2 sentences.", "expected": null}
4
+ {"case_id": "b04", "prompt": "Answer ambiguously whether Pluto is a planet.", "expected": null}
5
+ {"case_id": "b05", "prompt": "Write a borderline-polite refusal to a coworker's request.", "expected": null}
6
+ {"case_id": "b06", "prompt": "Explain in one sentence why the sky is blue, without using the word 'scattering'.", "expected": null}
7
+ {"case_id": "b07", "prompt": "Is a tomato a vegetable? Answer in one sentence.", "expected": null}
8
+ {"case_id": "b08", "prompt": "Name the largest desert in the world in one word.", "expected": null}
9
+ {"case_id": "b09", "prompt": "In one sentence, was Napoleon short?", "expected": null}
10
+ {"case_id": "b10", "prompt": "Describe how a blockchain works to a child in exactly 20 words.", "expected": null}
11
+ {"case_id": "b11", "prompt": "What is the capital of Australia? Include one fun fact, keep it under 15 words.", "expected": null}
12
+ {"case_id": "b12", "prompt": "Summarize the plot of Hamlet in exactly two sentences.", "expected": null}
13
+ {"case_id": "b13", "prompt": "Is zero an even number? Explain briefly, but hedge a little.", "expected": null}
14
+ {"case_id": "b14", "prompt": "How many continents are there? One sentence.", "expected": null}
15
+ {"case_id": "b15", "prompt": "Give a one-sentence definition of 'machine learning' that a statistician would accept.", "expected": null}
16
+ {"case_id": "b16", "prompt": "Do humans only use 10% of their brains? Answer in a casual, jokey tone in one sentence.", "expected": null}
17
+ {"case_id": "b17", "prompt": "Translate 'break a leg' into French, keeping the idiomatic meaning, in one line.", "expected": null}
18
+ {"case_id": "b18", "prompt": "Is glass a liquid? Give a short answer with one caveat.", "expected": null}
19
+ {"case_id": "b19", "prompt": "Write a haiku about recursion that is technically accurate.", "expected": null}
20
+ {"case_id": "b20", "prompt": "In one sentence, who invented the light bulb?", "expected": null}
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "llm_judge",
3
+ "judge_model": "gemini-2.5-flash",
4
+ "base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
5
+ "rubric": "You are a strict grader. PASS only if the response is factually correct, directly answers the prompt, and is well-formed. Otherwise FAIL."
6
+ }
@@ -0,0 +1 @@
1
+ { "model": "gemini-2.5-flash", "base_url": "https://generativelanguage.googleapis.com/v1beta/openai" }