probatio-llm 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 (53) hide show
  1. probatio_llm-0.1.0/.gitignore +35 -0
  2. probatio_llm-0.1.0/CHANGELOG.md +147 -0
  3. probatio_llm-0.1.0/LICENSE +21 -0
  4. probatio_llm-0.1.0/PKG-INFO +326 -0
  5. probatio_llm-0.1.0/README.md +290 -0
  6. probatio_llm-0.1.0/pyproject.toml +98 -0
  7. probatio_llm-0.1.0/src/probatio/__init__.py +69 -0
  8. probatio_llm-0.1.0/src/probatio/artefacts.py +201 -0
  9. probatio_llm-0.1.0/src/probatio/assertions/__init__.py +35 -0
  10. probatio_llm-0.1.0/src/probatio/assertions/backends.py +125 -0
  11. probatio_llm-0.1.0/src/probatio/assertions/contains.py +99 -0
  12. probatio_llm-0.1.0/src/probatio/assertions/judge.py +122 -0
  13. probatio_llm-0.1.0/src/probatio/assertions/result.py +44 -0
  14. probatio_llm-0.1.0/src/probatio/assertions/schema.py +144 -0
  15. probatio_llm-0.1.0/src/probatio/assertions/similarity.py +57 -0
  16. probatio_llm-0.1.0/src/probatio/budget.py +640 -0
  17. probatio_llm-0.1.0/src/probatio/case.py +374 -0
  18. probatio_llm-0.1.0/src/probatio/cassette.py +896 -0
  19. probatio_llm-0.1.0/src/probatio/cli.py +681 -0
  20. probatio_llm-0.1.0/src/probatio/collector.py +381 -0
  21. probatio_llm-0.1.0/src/probatio/errors.py +72 -0
  22. probatio_llm-0.1.0/src/probatio/hashing.py +95 -0
  23. probatio_llm-0.1.0/src/probatio/judge/__init__.py +56 -0
  24. probatio_llm-0.1.0/src/probatio/judge/core.py +221 -0
  25. probatio_llm-0.1.0/src/probatio/judge/kappa.py +92 -0
  26. probatio_llm-0.1.0/src/probatio/judge/prompt.py +91 -0
  27. probatio_llm-0.1.0/src/probatio/judge/rubric.py +122 -0
  28. probatio_llm-0.1.0/src/probatio/judge/validation.py +175 -0
  29. probatio_llm-0.1.0/src/probatio/metamorphic/__init__.py +109 -0
  30. probatio_llm-0.1.0/src/probatio/metamorphic/base.py +230 -0
  31. probatio_llm-0.1.0/src/probatio/metamorphic/evaluate.py +124 -0
  32. probatio_llm-0.1.0/src/probatio/metamorphic/freeze.py +278 -0
  33. probatio_llm-0.1.0/src/probatio/metamorphic/relations.py +658 -0
  34. probatio_llm-0.1.0/src/probatio/metamorphic/variants.py +219 -0
  35. probatio_llm-0.1.0/src/probatio/plugin.py +592 -0
  36. probatio_llm-0.1.0/src/probatio/providers/__init__.py +28 -0
  37. probatio_llm-0.1.0/src/probatio/providers/anthropic.py +172 -0
  38. probatio_llm-0.1.0/src/probatio/providers/base.py +68 -0
  39. probatio_llm-0.1.0/src/probatio/providers/claude_cli.py +287 -0
  40. probatio_llm-0.1.0/src/probatio/providers/fake.py +186 -0
  41. probatio_llm-0.1.0/src/probatio/py.typed +0 -0
  42. probatio_llm-0.1.0/src/probatio/reporters/__init__.py +37 -0
  43. probatio_llm-0.1.0/src/probatio/reporters/junit.py +219 -0
  44. probatio_llm-0.1.0/src/probatio/reporters/markdown.py +97 -0
  45. probatio_llm-0.1.0/src/probatio/reporters/results.py +66 -0
  46. probatio_llm-0.1.0/src/probatio/reporters/tables.py +196 -0
  47. probatio_llm-0.1.0/src/probatio/reporters/terminal.py +82 -0
  48. probatio_llm-0.1.0/src/probatio/runner.py +87 -0
  49. probatio_llm-0.1.0/src/probatio/session.py +490 -0
  50. probatio_llm-0.1.0/src/probatio/snapshot.py +515 -0
  51. probatio_llm-0.1.0/src/probatio/stability/__init__.py +40 -0
  52. probatio_llm-0.1.0/src/probatio/stability/stats.py +229 -0
  53. probatio_llm-0.1.0/src/probatio/stability/wilson.py +56 -0
@@ -0,0 +1,35 @@
1
+ # Byte-compiled / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Virtual environments
7
+ .venv/
8
+ venv/
9
+ env/
10
+
11
+ # Build artefacts
12
+ build/
13
+ dist/
14
+ *.egg-info/
15
+ .eggs/
16
+
17
+ # Tooling caches
18
+ .pytest_cache/
19
+ .ruff_cache/
20
+ .mypy_cache/
21
+ .hypothesis/
22
+
23
+ # Coverage
24
+ .coverage
25
+ .coverage.*
26
+ htmlcov/
27
+ coverage.xml
28
+
29
+ # Editors and OS
30
+ .idea/
31
+ .vscode/
32
+ .DS_Store
33
+
34
+ # NOTE: .probatio/ is deliberately NOT ignored. It holds committed baselines and
35
+ # judge validation records, which CI must read.
@@ -0,0 +1,147 @@
1
+ # Changelog
2
+
3
+ Notable changes to Probatio, newest first. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [semantic versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ The `Added` list below is in the order of the specification's architecture section, which is also
8
+ the order in which the phases recorded in [`PROGRESS.md`](PROGRESS.md) built it.
9
+
10
+ This file states no measurement. Every figure Probatio has produced about itself is printed in
11
+ [`README.md`](README.md) and under [`docs/`](docs/), and indexed against the committed file that
12
+ produced it in [`docs/PROVENANCE.md`](docs/PROVENANCE.md); repeating a number here would be a
13
+ second copy with no test behind it.
14
+
15
+ ## [0.1.0] — 2026-09-06
16
+
17
+ First release. Probatio is a pytest plugin for regression-testing LLM applications, distributed as
18
+ `probatio-llm` and imported as `probatio`. Its two headline features are metamorphic relations and
19
+ flakiness statistics; everything else on this list exists so that those two are usable from a
20
+ normal test suite.
21
+
22
+ ### Added
23
+
24
+ - **Foundations.** `ProbatioError` and the seven subclasses that name the offending case and, where
25
+ one exists, the command that fixes it: `ProbatioConfigError`, `MissingCassetteError`,
26
+ `StaleCassetteError`, `MissingVariantsError`, `BaselineDriftError`, `BudgetExceededError`,
27
+ `JudgeOutputError`. `stable_hash`, the canonical-JSON content hash behind every persisted
28
+ identity in the tool — cassette keys, prompt hashes, variant provenance, label files — stable
29
+ across processes and platforms.
30
+ - **Cases.** `LLMCase`, a pydantic model with an id, a string-or-mapping input, an optional system
31
+ prompt and parameters, its assertions, an optional budget, a snapshot mode and tags; and
32
+ `load_cases`, which loads a directory of YAML files in filename order and reports a malformed
33
+ file by name and field rather than by traceback.
34
+ - **Providers.** The `Provider` protocol and the `Completion` it returns, carrying text, token
35
+ counts, latency and a cost that is `None` when the provider cannot price the call rather than a
36
+ zero a ceiling would silently pass. Four adapters: `FakeProvider` and `ScriptedProvider` for
37
+ tests, `AnthropicProvider` over the SDK behind the `anthropic` extra, and `ClaudeCLIProvider`,
38
+ which runs the Claude Code CLI as a subprocess in a fresh empty directory so a developer on a
39
+ Claude plan can record without an API key. `--probatio-timeout` sets the subprocess ceiling.
40
+ - **Assertions.** Five checks over one `AssertionResult`: `schema_valid` (JSON Schema, with fenced
41
+ blocks stripped), `contains` and `not_contains`, `similarity` against a reference, and `judge`.
42
+ Similarity ships a pure-Python trigram-cosine backend and a `SimilarityBackend` protocol for
43
+ pluggable embeddings; nothing is downloaded, and no backend but the default is ever installed or
44
+ invoked by a test run.
45
+ - **Judge and validation.** `Judge` grades a case against a markdown rubric resolved from
46
+ `rubrics/`; the prompt template is a module constant whose hash is part of every judge cassette
47
+ key. A reply that is not strict JSON is asked again a bounded number of times and then returned
48
+ as a failed assertion, never raised. `cohens_kappa` and the validation record it writes;
49
+ `probatio validate-judge` measures a judge against human labels, from two existing columns or by
50
+ running the judge over the rows, and exits non-zero below `--min-kappa`. A judge with no
51
+ validation record on disk is reported as unenforceable rather than trusted.
52
+ - **Snapshots.** Per-case baselines under `.probatio/baseline/`, in `scores` or `output` mode,
53
+ keyed on a hash of the prompt so a case whose input changed is reported as a new baseline rather
54
+ than as drift. `--update-baseline` rewrites them; `--baseline-dir` moves them.
55
+ - **Budgets.** Per-case `max_cost_usd` and `max_latency_ms`, suite-wide `--max-cost` and
56
+ `--max-latency`, and `PriceTable` loaded from `--probatio-prices`. **The unenforceable rule:** a
57
+ cost ceiling evaluated where any call has an unknown cost is reported under warnings and not
58
+ under passed checks, because a ceiling that cannot be evaluated has not been met. No prices ship
59
+ with the package.
60
+ - **Cassettes.** `CassetteProvider` wraps any provider in `replay`, `record` or `off` mode, with
61
+ `replay` the default, so a suite runs from committed tapes and makes no model call. Tapes are
62
+ keyed on prompt, system, model, parameters and — for a judge call — the template hash, so an
63
+ edited prompt or rubric is detected as a stale tape rather than replayed as if nothing changed.
64
+ Missing and stale tapes are both raised and carried into the report. `--cassette-dir` moves the
65
+ store; `probatio import-cassettes` builds one from a JSONL of interactions recorded elsewhere.
66
+ - **Metamorphic relations.** The `Relation` base and its registry, and four decorators:
67
+ `@paraphrase_invariant`, `@order_invariant`, `@distractor_robust` and `@format_jitter`. Each
68
+ generates variants of a case that should not change its verdict; the plugin runs them and reports
69
+ a per-relation violation rate with the individual flips, and prints *not applicable* rather than
70
+ a rate for a relation whose case does not carry the field it needs. Paraphrases are not generated
71
+ at test time: `probatio freeze-variants` asks a model once, a human reviews and edits the
72
+ written files, and the suite reads the frozen, committed variants.
73
+ - **Flakiness statistics.** `--runs N` repeats each case in place and reports a per-case pass rate
74
+ with a hand-implemented Wilson 95% interval, a majority verdict, a suite stability score, and the
75
+ number of cases whose interval falls below their declared floor. `@flaky_tolerant(p=…)` declares
76
+ that floor per case; a case that declares none is held to a floor of 1.0.
77
+ - **Reporters.** One `RunReport` rendered four ways: the terminal section pytest prints at the end
78
+ of every run, a GitHub-flavoured markdown report written to `$GITHUB_STEP_SUMMARY` or
79
+ `--probatio-report`, JUnit XML at `--probatio-junit` carrying pass rates, interval bounds, costs
80
+ and per-relation violation rates as properties, and the full report as JSON at
81
+ `--probatio-results`. A quantity that was not measured is omitted, never written as a zero.
82
+ - **The pytest surface.** The `probatio`, `provider` and `judge_provider` fixtures; the
83
+ `probatio_relation` and `flaky_tolerant` markers; and the option group that selects the provider
84
+ and model, the judge provider and model, the cassette mode and store, the baseline directory,
85
+ the price table, the ceilings, the timeout and the three report paths. A non-fake provider with
86
+ no model named is refused before the session starts.
87
+ - **The `probatio` console script.** `validate-judge`, `freeze-variants` and `import-cassettes`.
88
+ These are the only entry points that call a live model, and only when a human runs them.
89
+ - **Packaging.** `probatio-llm` on Python 3.11 and newer, with runtime dependencies of exactly
90
+ `pytest`, `pydantic`, `jsonschema` and `PyYAML`; an `anthropic` extra for the SDK adapter and a
91
+ `dev` extra for the gate. Ships `py.typed`: the package is typed and `mypy --strict` is part of
92
+ the gate. The sdist carries the package, this file, the README and the licence, and neither
93
+ `tests/` nor `examples/`.
94
+ - **Examples.** `examples/demo_suite/`, the executable design specification, written before the
95
+ implementation and byte-frozen since — every decorator, every assertion type and a deliberate
96
+ failing case, runnable offline. `examples/consilium/`, a dogfood suite over a real
97
+ retrieval-augmented medical Q&A system, offline from committed tapes, with a live variant used
98
+ to record them.
99
+ - **Documentation.** `README.md` with a prior-art comparison against DeepEval, promptfoo, Giskard,
100
+ Ragas, Braintrust and LangSmith; `docs/DESIGN.md`, `docs/assertions.md`, `docs/relations.md`,
101
+ `docs/stability.md`, `docs/providers.md`, `docs/EVALUATION.md`, `docs/CASE_STUDY.md`; and
102
+ `docs/PROVENANCE.md`, which indexes every number in the documentation to the committed file that
103
+ produced it and is enforced by a test that re-derives each one.
104
+ - **Continuous integration.** The five-part quality gate — pytest, coverage of `src/probatio`,
105
+ ruff, `mypy --strict`, and byte-identity of the frozen demo suite — on the floor version and the
106
+ one above it, with no credentials and no network access to a model provider.
107
+
108
+ ### Known limitations
109
+
110
+ Stated in full, with the evidence, in [`docs/CASE_STUDY.md`](docs/CASE_STUDY.md) §5 and
111
+ [`docs/EVALUATION.md`](docs/EVALUATION.md); summarised here because they bound what this release
112
+ has shown.
113
+
114
+ - **The judge in this repository is not validated on disk, deliberately.** Its two measured kappas
115
+ disagree with each other, and neither is high; `docs/EVALUATION.md` §4 gives both and says why no
116
+ validation record was written. Every judge verdict in the case study therefore carries
117
+ `unenforceable=True`, and the relation rates that depend on those verdicts inherit it. They are
118
+ reported because an unenforceable result is still a measurement — of an unvalidated instrument.
119
+ - **A suite cannot yet declare a minimum kappa that fails the build.**
120
+ `probatio validate-judge --min-kappa` exits non-zero from the command line, but there is no
121
+ equivalent of a budget overrun inside a pytest run.
122
+ - **`--runs N` repeats inside one pytest item, so function-scoped fixtures are shared across the
123
+ runs.** That is deliberate — it measures the model's nondeterminism, not state leakage — but a
124
+ suite wanting a fresh fixture per run has to build one inside its own callable
125
+ (`docs/stability.md`).
126
+ - **The case study's live system under test is not the system it borrows its questions from.** It
127
+ makes one grounded call with the corpus notes already in hand; the real pipeline plans,
128
+ retrieves, runs agents and repairs. Retrieval is not modelled at all, so nothing here speaks to
129
+ retrieval quality.
130
+ - **The case study is a small sample, run once, on one model family.** Fifteen cases, one run
131
+ each, two Anthropic models reached through the same adapter. No interval in that document is a
132
+ confidence interval, and the flip pattern most in need of repetition is the one repetition would
133
+ have resolved.
134
+ - **Route A of the case study is retrospective.** The regression it replays was found and
135
+ documented by the system's own evaluation before the suite was written, and the suite's
136
+ assertions were derived from that system's golden labels and phrase list.
137
+ - **Costs reported through `ClaudeCLIProvider` are notional.** They are the API price the CLI
138
+ computes for calls made on a subscription that was not billed that money;
139
+ `docs/providers.md` says so wherever the figure is used.
140
+ - **Anthropic is the only live provider.** There is no OpenAI adapter and none is planned; another
141
+ provider is a one-class extension, documented in `docs/providers.md`.
142
+ - **Similarity's default backend is lexical.** Trigram cosine is deterministic and needs no
143
+ download, which is why it is the default; it is not a semantic measure, and
144
+ `docs/assertions.md` publishes the band it produced on real answers instead of recommending a
145
+ threshold.
146
+
147
+ [0.1.0]: https://github.com/Lexieli666/probatio/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yichen Li
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,326 @@
1
+ Metadata-Version: 2.5
2
+ Name: probatio-llm
3
+ Version: 0.1.0
4
+ Summary: A pytest plugin for regression-testing LLM applications: metamorphic relations and flakiness statistics.
5
+ Project-URL: Homepage, https://github.com/Lexieli666/probatio
6
+ Project-URL: Repository, https://github.com/Lexieli666/probatio
7
+ Project-URL: Changelog, https://github.com/Lexieli666/probatio/blob/main/CHANGELOG.md
8
+ Author-email: Yichen Li <lexieli@seas.upenn.edu>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: llm,metamorphic-testing,pytest,regression-testing,testing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: jsonschema>=4
22
+ Requires-Dist: pydantic>=2
23
+ Requires-Dist: pytest>=8
24
+ Requires-Dist: pyyaml>=6
25
+ Provides-Extra: anthropic
26
+ Requires-Dist: anthropic>=0.40; extra == 'anthropic'
27
+ Provides-Extra: dev
28
+ Requires-Dist: build; extra == 'dev'
29
+ Requires-Dist: coverage; extra == 'dev'
30
+ Requires-Dist: mypy; extra == 'dev'
31
+ Requires-Dist: ruff; extra == 'dev'
32
+ Requires-Dist: twine; extra == 'dev'
33
+ Requires-Dist: types-jsonschema; extra == 'dev'
34
+ Requires-Dist: types-pyyaml; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # Probatio
38
+
39
+ Probatio is a **pytest plugin for regression-testing LLM applications**. You write cases in YAML,
40
+ one test function per suite, and keep running the suite you already have. It adds two disciplines
41
+ the software-testing literature has and LLM developer tooling mostly does not: **metamorphic
42
+ relations**, decorators that assert a semantics-preserving change to an input does not change the
43
+ verdict, reported as a per-relation violation rate; and **flakiness statistics**, repeated
44
+ execution with a per-case pass rate, a Wilson 95% interval and a suite stability score, instead of
45
+ running a nondeterministic system once and printing a tick. Everything else it ships — JSON-schema
46
+ validity, contains and not-contains assertions, similarity, a rubric judge with Cohen's κ against
47
+ human labels, snapshot baselines, cost and latency ceilings, record-replay cassettes, and
48
+ terminal, GitHub-summary, JUnit-XML and JSON reporters — is there so those two are usable on a
49
+ real suite. The judge shipped here has **measured agreement** with human labels, not validated
50
+ agreement; §*Case study* says what was measured and what it came to.
51
+
52
+ ## Quick start
53
+
54
+ ```bash
55
+ pip install probatio-llm
56
+ ```
57
+
58
+ ```yaml
59
+ # examples/demo_suite/cases/06-copd-spirometry.yaml
60
+ id: copd-spirometry
61
+ input: "In one sentence, what test confirms a diagnosis of COPD?"
62
+ assertions:
63
+ - {type: contains, any: ["spirometry"]}
64
+ ```
65
+
66
+ ```python
67
+ # examples/demo_suite/test_demo.py
68
+ CASES = load_cases(HERE / "cases")
69
+
70
+
71
+ @pytest.mark.parametrize("case", CASES, ids=lambda c: c.id)
72
+ @format_jitter(field="input.question")
73
+ def test_case(case, probatio, provider):
74
+ probatio.check(case, sut=lambda c: answer(c, provider))
75
+ ```
76
+
77
+ ```bash
78
+ pytest examples/demo_suite --runs 5
79
+ ```
80
+
81
+ That is shortened from [`examples/demo_suite/`](examples/demo_suite/), the executable
82
+ specification this tool was built against and which has not been edited since it was written. Every
83
+ line above is verbatim from it except one: the real test parametrizes over a filtered subset of
84
+ `CASES` rather than over `CASES` itself, because the demo routes its expected-fail and flaky cases
85
+ to their own test functions. What is left out is the rest of that case — a second assertion, a
86
+ budget, its tags — and the three other relations the real `test_case` carries. Note what the shown
87
+ case does *not* exercise: its `input` is a bare string, so `@format_jitter(field="input.question")`
88
+ reports **not applicable** on it rather than `0.00`, and
89
+ [`examples/demo_suite/cases/01-htn-definition.yaml`](examples/demo_suite/cases/01-htn-definition.yaml)
90
+ is the dict-input case, with a `question` and a list of `documents`, that the field relations
91
+ actually apply to.
92
+
93
+ ## Metamorphic relations
94
+
95
+ A relation names a transformation that should preserve a case's meaning and asserts the verdict
96
+ does not move. Four ship as decorators — `@order_invariant`, `@paraphrase_invariant`,
97
+ `@distractor_robust`, `@format_jitter` — and each reports a violation rate **alongside** the case,
98
+ never folded into its pass or fail. A violation counts in either direction: a variant that passes
99
+ where the original failed breaks the invariant just as much.
100
+
101
+ From `pytest examples/demo_suite --runs 5`, run at commit `4c2114e` and recorded verbatim in
102
+ [`PROGRESS.md`](PROGRESS.md):
103
+
104
+ ```
105
+ relations:
106
+ relation cases n/a violations mean rate worst case worst rate
107
+ -------------------- ----- --- ---------- --------- ------------------- ----------
108
+ distractor_robust 7 1 0/70 0.00 gerd-alarm-features 0.00
109
+ format_jitter 7 1 15/105 0.14 htn-definition 0.33
110
+ order_invariant 6 2 0/50 0.00 gerd-alarm-features 0.00
111
+ paraphrase_invariant 2 0 5/30 0.17 htn-definition 0.33
112
+ ```
113
+
114
+ The `n/a` column is the point of the design: a relation a case is out of scope for — reordering a
115
+ one-element document list, rewording a case whose input is a bare string — reports *not
116
+ applicable*, never `0.00`. Paraphrases are generated once by `probatio freeze-variants`, reviewed
117
+ by a human and committed; they are never sampled during a test run.
118
+
119
+ ## Flakiness statistics
120
+
121
+ `--runs N` runs each case N times and reports what happened, rather than what happened once. Same
122
+ run, same commit:
123
+
124
+ ```
125
+ cases:
126
+ case verdict assertions pass rate 95% Wilson floor cost latency ms snapshot
127
+ --------------------- ------- ---------- --------- ------------ ----- --------- ---------- ---------
128
+ htn-definition pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
129
+ htn-first-line pass 3/3 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
130
+ t2d-screening-json pass 2/2 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
131
+ t2d-metformin pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
132
+ gerd-alarm-features pass 3/3 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
133
+ copd-spirometry pass 2/2 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
134
+ red-flag-chest-pain pass 3/3 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
135
+ insomnia-first-line pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
136
+ htn-definition pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
137
+ t2d-metformin pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
138
+ anxiety-expected-fail FAIL 1/3 0.00 [0.00, 0.43] 1.00 $0.000500 100 -
139
+ flu-antivirals-flaky pass 2/2 0.80 [0.38, 0.96] 0.80 $0.000500 100 -
140
+ ```
141
+
142
+ ```
143
+ stability score: 0.90 over 12 repeated case(s)
144
+ cases whose Wilson lower bound is below their floor: 12 of 12
145
+ cost: $0.031500 (no --max-cost ceiling)
146
+ ```
147
+
148
+ `flu-antivirals-flaky` declares `@flaky_tolerant(p=0.8, n=5)`, passes four of its five runs, and
149
+ passes the case at the bar its author wrote down; `anxiety-expected-fail` fails every run and is
150
+ in the suite so the report has a failure to show. The stability score is the mean of those twelve
151
+ pass rates.
152
+
153
+ **"12 of 12" is not twelve problems.** It counts the repeated cases whose Wilson *lower* bound
154
+ falls under their floor, and at n = 5 the lower bound of a perfect five-for-five is 0.57, so no
155
+ case can clear a floor of 1.0 however well it does — the line is a distance from the suite's own
156
+ floors, and it becomes informative when `--runs` is large enough for a bound to reach one
157
+ ([`docs/stability.md`](docs/stability.md)). Nothing in it decides a verdict: the observed pass
158
+ rate does that.
159
+
160
+ The run also names what it could not check:
161
+
162
+ ```
163
+ warnings (7):
164
+ - htn-definition: 1 judge verdict(s) from a rubric with no validation record (run: probatio validate-judge --labels <csv> --rubric <name>)
165
+ - htn-first-line: 1 judge verdict(s) from a rubric with no validation record (run: probatio validate-judge --labels <csv> --rubric <name>)
166
+ ```
167
+
168
+ — and five more like them, one per judged case. A judge with no validation record on disk is not a
169
+ validated judge, and the tool says so about every suite, including its own.
170
+
171
+ ## Prior art
172
+
173
+ The field is crowded and this table is the honest version of where Probatio sits. Every claim in
174
+ it is what that tool's own documentation says, **checked against each tool's documentation on
175
+ 2026-09-05**; "not documented" means the page that would carry the feature was read and does not
176
+ carry it, not that the feature is absent.
177
+
178
+ | tool | what it is (their words) | pytest | repeated runs | interval on a per-case pass rate | metamorphic relations | JUnit XML |
179
+ |---|---|---|---|---|---|---|
180
+ | **DeepEval** | "plugs into Pytest, so `deepeval test run` collects and runs your eval files the same way pytest would"; a large catalog of "research-backed LLM-as-a-Judge" metrics | yes, native (`assert_test`) | not documented | none documented; a **flaky metric** flag instead | none documented | not documented |
181
+ | **promptfoo** | "open-source CLI and library for evaluating and red-teaming LLM apps" | no | **yes**, `--repeat <number>` | none documented | none documented | **yes**, `--output junit.xml` |
182
+ | **Giskard** | "enterprise platform and open-source library for LLM evaluation and security" | yes; behavioural tests "pass or fail under pytest" | not documented | none documented | the word does not appear; its scan is adversarial generation | not documented |
183
+ | **Ragas** | "move from 'vibe checks' to systematic evaluation loops"; RAG metrics | not documented | not documented | none documented | none documented | not documented |
184
+ | **Braintrust** | "the active observability platform for instrumenting, understanding, and improving agents" | not documented | not documented | none documented | none documented | not documented |
185
+ | **LangSmith** | "a framework for measuring quality throughout the application lifecycle" | **yes**, `@pytest.mark.langsmith` | **yes**, `num_repetitions` | mean and standard deviation; no interval documented | none documented | not documented |
186
+ | **Probatio** | this repository | yes, a plugin | yes, `--runs N` | Wilson 95% per case, plus a suite stability score and a floor marker | four, as decorators, with frozen variants | yes, with the statistics as `<properties>` |
187
+
188
+ Pages read on 2026-09-05: `deepeval.com/docs/getting-started` and `/docs/metrics-introduction`;
189
+ `promptfoo.dev/docs/intro` and `/docs/usage/command-line`; `docs.giskard.ai`;
190
+ `docs.ragas.io/en/stable`; `braintrust.dev/docs` and `/docs/guides/evals`;
191
+ `docs.langchain.com/langsmith/evaluation-concepts`, `/langsmith/pytest`, `/langsmith/repetition`
192
+ and `/langsmith/evaluate-llm-application`.
193
+
194
+ **What is not a differentiator, said plainly.** Being pytest-native is not one: DeepEval, Giskard
195
+ and LangSmith all are, and DeepEval owns that ground with a far larger metric catalog. Running a
196
+ case repeatedly is not one: promptfoo has `--repeat` and LangSmith has `num_repetitions`. JUnit
197
+ output is not one: promptfoo writes `junit.xml`. If you want a metric catalog, use DeepEval; if
198
+ you want a red-teaming engine, use promptfoo or Giskard; if you want hosted eval tracing, use
199
+ Braintrust or LangSmith. Probatio is meant to sit beside them.
200
+
201
+ **What is.** A confidence interval on a per-case pass rate, a suite-level stability score, and a
202
+ floor marker (`@flaky_tolerant(p, n)`) that holds a case to a number somebody wrote down — none of
203
+ the six documents any of these. And metamorphic relations as stated invariants, declared as
204
+ decorators on the test: none of the six documents those either, and Giskard's scan is adversarial
205
+ generation, which searches for inputs that break a system rather than checking a property the
206
+ system is supposed to have. The one-sentence contrast that matters: **DeepEval's answer to a flaky
207
+ metric is a flag that makes its failure non-deciding; Probatio's is to run the case n times and
208
+ report the rate, the interval and the floor.**
209
+
210
+ Metamorphic testing of LLMs exists as research, and Probatio claims no new relation — its four are
211
+ packaging, with reviewable frozen variants, of transformations these papers name. LLMorph
212
+ `[LLMORPH]` and MTF `[MTF]` are the two research frameworks in this space; the NLP catalogue
213
+ `[MR-CATALOG-NLP]` collected **191** metamorphic relations in a literature review, of which 36
214
+ were implemented, and it is the source `order_invariant` and `paraphrase_invariant` cite. Chen et
215
+ al. `[CHEN-MT-SURVEY]` and Segura et al. `[SEGURA-MT-SURVEY]` are the surveys for the definition
216
+ of a relation and for violation-as-oracle. Full entries, with authors, venues, years and DOIs
217
+ verified against Crossref on 2026-09-05, are in [`docs/relations.md`](docs/relations.md).
218
+
219
+ ## Case study
220
+
221
+ Probatio was dogfooded on
222
+ [Consilium-Health](https://github.com/Lexieli666/consilium-health), the author's own
223
+ public multi-agent retrieval-grounded
224
+ question-answering project, along two routes. Both are written up with their limitations in
225
+ [`docs/CASE_STUDY.md`](docs/CASE_STUDY.md), and every number below is re-derived from a committed
226
+ artefact by `tests/test_docs_case_study.py`.
227
+
228
+ **Route A — a retrospective catch, and the headline claim.** Fifteen of Consilium's golden items
229
+ replay through Probatio from its published traces, against two recorded configurations. On
230
+ `baseline_llm`, a single call with no retrieval and no agents, all fifteen pass. On the
231
+ multi-agent `full` pipeline, **five of the six red-flag cases fail** — each on a `contains`
232
+ assertion carrying Consilium's own thirty-eight escalation phrases, and on nothing else — while
233
+ **none fails on `baseline_llm`**. That is a safety regression Consilium had already documented in
234
+ its own `docs/FAILURE_CASES.md`, reproduced as a failing `pytest` run from committed tapes with no
235
+ model called. It is labelled retrospective in the case study, because it is: the suite was written
236
+ after the regression was known.
237
+
238
+ **Route B — prospective in form.** One flag changed, `claude-opus-5` to
239
+ `claude-haiku-4-5-20251001`, on a live single-call system under test, with nothing else moved.
240
+ The suite reported that **4 of 15 verdicts moved**, all of them on the judge; that the judge marked
241
+ **a third of the cases** (5 of 15) unfaithful to their sources, naming the sentence in each; that
242
+ eight snapshots drifted, three of them on cases whose every assertion still passed; that
243
+ `paraphrase_invariant` went from 0.04 to 0.30 under rewordings a human had confirmed do not change
244
+ the question; and that the cheaper recording cost **$0.987356 against $6.037457** in notional API
245
+ price, a six-fold difference. It is prospective in form — suite first, change second, outcome
246
+ unknown in advance — and it is on a system under test built for the purpose, not on a shipping
247
+ application. The case study says so in those words.
248
+
249
+ Dogfooding also found three things wrong with Probatio itself — two defects and one gap that nine
250
+ phases of unit tests had not reached — all fixed during Phase 12 and listed with the commit that
251
+ fixed each in [`docs/EVALUATION.md`](docs/EVALUATION.md) §7.
252
+
253
+ ## Design positions
254
+
255
+ **Similarity is for paraphrase-stable content; a judge is for claims.** Reach for `similarity`
256
+ when what you are protecting is wording that should stay roughly the same, and for a `judge` when
257
+ it is whether a claim is true, grounded or complete. The docs take that position rather than
258
+ listing options, because the substitution fails in a way a threshold cannot fix: a trigram cosine
259
+ scores a sentence that *contradicts* the reference at 0.223 while the same claim in different
260
+ words scores 0.453, so the ordering is wrong, not the number
261
+ ([`docs/assertions.md`](docs/assertions.md), rationale in [`docs/DESIGN.md`](docs/DESIGN.md)).
262
+
263
+ **An unenforceable ceiling is never a pass.** A `max_cost_usd` ceiling evaluated where any call's
264
+ cost is unknown cannot be checked, so it is reported as unenforceable, listed under warnings, and
265
+ excluded from passing checks — never quietly satisfied by treating unknown as zero. The same holds
266
+ for a case that recorded no provider calls at all: a check that cannot fail is not a check. This
267
+ is the failure mode a testing tool must not have, and it is why `Completion.cost_usd` is `None`
268
+ rather than `0.0` when nobody priced the call (`docs/DESIGN.md`, Phases 6 and 11).
269
+
270
+ **Frozen variants.** Paraphrases are generated once by `probatio freeze-variants`, written to
271
+ `variants/<case_id>.yaml` with their provenance, reviewed, and committed. Three reasons in order:
272
+ the suite stays deterministic; the model is not part of the oracle at test time; and the variants
273
+ are in the diff, so a reviewer can throw out a rewording that changed the meaning. That last one is
274
+ not hypothetical — **2 of 45** frozen paraphrases were deleted at review in this project, each
275
+ noted in its file's header ([`docs/EVALUATION.md`](docs/EVALUATION.md) §3).
276
+
277
+ **A plugin, not a platform.** Teams do not migrate eval platforms mid-quarter; they add a
278
+ dependency to the tests they already run. So Probatio is four runtime dependencies, one entry
279
+ point, a `Provider` protocol of one method, and reporters that write into CI you already have —
280
+ JUnit XML and a GitHub job summary — rather than a dashboard to log into. No hosted service, no
281
+ async, no metric catalog (`docs/DESIGN.md`, Phase 0).
282
+
283
+ ## Install
284
+
285
+ The distribution is named `probatio-llm`; the import name is `probatio`.
286
+
287
+ ```bash
288
+ pip install probatio-llm # core: pytest, pydantic, jsonschema, PyYAML
289
+ pip install "probatio-llm[anthropic]" # plus the Anthropic SDK adapter
290
+ pip install "probatio-llm[dev]" # ruff, mypy, coverage, build, twine
291
+ ```
292
+
293
+ Python 3.11 or newer. The `claude-cli` provider needs no extra and no API key — it runs the Claude
294
+ Code CLI as a subprocess, so a developer on a Claude plan can record cassettes and freeze variants;
295
+ the cost it reports is *notional* API price, not money billed, and
296
+ [`docs/providers.md`](docs/providers.md) says so wherever that figure is used.
297
+
298
+ > The unrelated PyPI placeholder distribution `probatio` also installs a `probatio/` import
299
+ > package. Do not install both into the same environment.
300
+
301
+ ## Status and roadmap
302
+
303
+ Pre-1.0. Phase-by-phase state, with the gate result and the run log for each phase, is in
304
+ [`PROGRESS.md`](PROGRESS.md); every judgement call made without asking is numbered in
305
+ [`DECISIONS.md`](DECISIONS.md); anything abandoned is in [`BLOCKERS.md`](BLOCKERS.md). Every number
306
+ in this file and under `docs/` is indexed in [`docs/PROVENANCE.md`](docs/PROVENANCE.md) with the
307
+ committed file it came from and the command that regenerates it.
308
+
309
+ Next, in the order they would be built:
310
+
311
+ - **Per-run fixture isolation.** `--runs N` repeats inside one pytest item, so function-scoped
312
+ fixtures are shared across the runs. That is deliberate — it measures the model's
313
+ nondeterminism, not state leakage — but a suite that wants a fresh fixture per run has to build
314
+ it inside its own callable today (`docs/stability.md`).
315
+ - **A minimum-κ gate.** `probatio validate-judge --min-kappa` already exits non-zero, but a suite
316
+ cannot yet declare a floor that fails the build the way a budget overrun does. The judge in this
317
+ repository scored κ 0.600 on one blind forty-row sample and 0.253 on another, which is the
318
+ argument for the gate and for reporting the number rather than the adjective.
319
+ - **The variance study.** The `--runs` engine is also an instrument: run a benchmark subset n
320
+ times and measure how often rankings flip. Seeded as an optional phase in `PROGRESS.md`, and
321
+ the one thing that would settle whether the judge or the answer is the unstable half of this
322
+ project's own relation rates (`docs/EVALUATION.md` §2).
323
+
324
+ ## License
325
+
326
+ MIT. See [`LICENSE`](LICENSE).