proofstep-core 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 (39) hide show
  1. proofstep_core-0.1.0/.gitignore +57 -0
  2. proofstep_core-0.1.0/PKG-INFO +39 -0
  3. proofstep_core-0.1.0/README.md +17 -0
  4. proofstep_core-0.1.0/pyproject.toml +38 -0
  5. proofstep_core-0.1.0/src/proofstep_core/__init__.py +59 -0
  6. proofstep_core-0.1.0/src/proofstep_core/aggregate.py +140 -0
  7. proofstep_core-0.1.0/src/proofstep_core/calibration.py +849 -0
  8. proofstep_core-0.1.0/src/proofstep_core/calibration_runner.py +471 -0
  9. proofstep_core-0.1.0/src/proofstep_core/compare.py +205 -0
  10. proofstep_core-0.1.0/src/proofstep_core/dataset.py +174 -0
  11. proofstep_core-0.1.0/src/proofstep_core/evaluators/__init__.py +47 -0
  12. proofstep_core-0.1.0/src/proofstep_core/evaluators/deterministic.py +404 -0
  13. proofstep_core-0.1.0/src/proofstep_core/evaluators/judge.py +335 -0
  14. proofstep_core-0.1.0/src/proofstep_core/evaluators/operational.py +127 -0
  15. proofstep_core-0.1.0/src/proofstep_core/evaluators/statistical.py +434 -0
  16. proofstep_core-0.1.0/src/proofstep_core/gates.py +545 -0
  17. proofstep_core-0.1.0/src/proofstep_core/paths.py +98 -0
  18. proofstep_core-0.1.0/src/proofstep_core/py.typed +0 -0
  19. proofstep_core-0.1.0/src/proofstep_core/redaction.py +263 -0
  20. proofstep_core-0.1.0/src/proofstep_core/runner.py +482 -0
  21. proofstep_core-0.1.0/src/proofstep_core/sampling.py +200 -0
  22. proofstep_core-0.1.0/src/proofstep_core/significance.py +477 -0
  23. proofstep_core-0.1.0/src/proofstep_core/stats.py +142 -0
  24. proofstep_core-0.1.0/src/proofstep_core/suite.py +277 -0
  25. proofstep_core-0.1.0/src/proofstep_core/testing.py +118 -0
  26. proofstep_core-0.1.0/src/proofstep_core/types.py +156 -0
  27. proofstep_core-0.1.0/src/proofstep_core/versioning.py +63 -0
  28. proofstep_core-0.1.0/tests/test_aggregate.py +103 -0
  29. proofstep_core-0.1.0/tests/test_calibration.py +622 -0
  30. proofstep_core-0.1.0/tests/test_compare.py +147 -0
  31. proofstep_core-0.1.0/tests/test_gates.py +349 -0
  32. proofstep_core-0.1.0/tests/test_gates_significance.py +201 -0
  33. proofstep_core-0.1.0/tests/test_hidden_regression.py +156 -0
  34. proofstep_core-0.1.0/tests/test_judge.py +195 -0
  35. proofstep_core-0.1.0/tests/test_runner.py +260 -0
  36. proofstep_core-0.1.0/tests/test_sampling.py +230 -0
  37. proofstep_core-0.1.0/tests/test_significance.py +337 -0
  38. proofstep_core-0.1.0/tests/test_statistical.py +181 -0
  39. proofstep_core-0.1.0/tests/test_stats.py +123 -0
@@ -0,0 +1,57 @@
1
+
2
+ __pycache__/
3
+ .coverage
4
+ .coverage.*
5
+ .docker-data/
6
+ .DS_Store
7
+ .e2e-api.log
8
+ .env
9
+ .env.*
10
+ .env.prod
11
+ # Exceptions, and they must come *after* the patterns above: git takes the last matching rule, so a
12
+ # negation written earlier in the file is silently overridden. That is not a hypothetical — the `!`
13
+ # line used to sit at the top, `.env.*` re-ignored it, and `.env.prod.example` was never committed.
14
+ # `scripts/init_secrets.sh` reads that file, so the first documented step of self-hosting failed for
15
+ # anyone who cloned the repository. It was caught by CI running the same step.
16
+ !.env.example
17
+ !.env.prod.example
18
+ .hypothesis/
19
+ .idea/
20
+ .mypy_cache/
21
+ .next/
22
+ .proofstep/
23
+ .pytest_cache/
24
+ .ruff_cache/
25
+ .turbo/
26
+ .venv/
27
+ .vscode/
28
+ *.egg-info/
29
+ *.key
30
+ *.pem
31
+ *.proofstep.local.yaml
32
+ *.py[cod]
33
+ *.swp
34
+ *.tsbuildinfo
35
+ # Database dumps. Never committed: they contain every tenant's data, and a backup in a git history
36
+ # Docker volumes
37
+ # Editors / OS
38
+ # is a backup with no access control and no retention.
39
+ # Node
40
+ # Proofstep local state
41
+ # Python
42
+ # Reports a local run drops in the working tree. The name comes from the suite, so the pattern has
43
+ # Secrets and local config — never commit these
44
+ # The e2e stack's server log, written next to the repo so a CI failure can print it.
45
+ # to cover all of them rather than the default filename only.
46
+ ~/.proofstep/
47
+ backups/
48
+ build/
49
+ coverage.xml
50
+ credentials.json
51
+ dist/
52
+ htmlcov/
53
+ node_modules/
54
+ out/
55
+ proofstep-*.json
56
+ secrets/
57
+ venv/
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.5
2
+ Name: proofstep-core
3
+ Version: 0.1.0
4
+ Summary: Proofstep evaluation engine — datasets, evaluators, runner, aggregation, gates
5
+ Project-URL: Homepage, https://github.com/IlaKhan17/proofstep
6
+ Project-URL: Documentation, https://github.com/IlaKhan17/proofstep/tree/main/docs
7
+ Project-URL: Repository, https://github.com/IlaKhan17/proofstep
8
+ Project-URL: Issues, https://github.com/IlaKhan17/proofstep/issues
9
+ License-Expression: Apache-2.0
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: proofstep-types
20
+ Requires-Dist: pydantic>=2.9
21
+ Description-Content-Type: text/markdown
22
+
23
+ # proofstep-core
24
+
25
+ **Evaluators, aggregation, gates, statistics** — part of [Proofstep](https://github.com/IlaKhan17/proofstep), the CI gate for AI
26
+ agents that knows the difference between a regression and a bad day.
27
+
28
+ The pure evaluation library: deterministic and statistical evaluators, LLM-judge harnesses with
29
+ calibration, score aggregation, quality gates, and paired significance testing.
30
+
31
+ No HTTP, no database, no provider SDKs — enforced in CI by an import-linter contract. That boundary
32
+ is what makes local mode, CI mode, and server mode the same code path, and it is why the CLI's exit
33
+ code and a dashboard's verdict cannot drift apart.
34
+
35
+ ## Documentation
36
+
37
+ Full documentation lives in the [repository](https://github.com/IlaKhan17/proofstep/tree/main/docs).
38
+
39
+ Apache-2.0.
@@ -0,0 +1,17 @@
1
+ # proofstep-core
2
+
3
+ **Evaluators, aggregation, gates, statistics** — part of [Proofstep](https://github.com/IlaKhan17/proofstep), the CI gate for AI
4
+ agents that knows the difference between a regression and a bad day.
5
+
6
+ The pure evaluation library: deterministic and statistical evaluators, LLM-judge harnesses with
7
+ calibration, score aggregation, quality gates, and paired significance testing.
8
+
9
+ No HTTP, no database, no provider SDKs — enforced in CI by an import-linter contract. That boundary
10
+ is what makes local mode, CI mode, and server mode the same code path, and it is why the CLI's exit
11
+ code and a dashboard's verdict cannot drift apart.
12
+
13
+ ## Documentation
14
+
15
+ Full documentation lives in the [repository](https://github.com/IlaKhan17/proofstep/tree/main/docs).
16
+
17
+ Apache-2.0.
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "proofstep-core"
3
+ version = "0.1.0"
4
+ description = "Proofstep evaluation engine — datasets, evaluators, runner, aggregation, gates"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "Apache-2.0"
8
+ classifiers = [
9
+ "Development Status :: 4 - Beta",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Topic :: Software Development :: Testing",
15
+ "Topic :: Software Development :: Quality Assurance",
16
+ "Typing :: Typed",
17
+ ]
18
+
19
+ dependencies = [
20
+ "proofstep-types",
21
+ "pydantic>=2.9",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/IlaKhan17/proofstep"
26
+ Documentation = "https://github.com/IlaKhan17/proofstep/tree/main/docs"
27
+ Repository = "https://github.com/IlaKhan17/proofstep"
28
+ Issues = "https://github.com/IlaKhan17/proofstep/issues"
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/proofstep_core"]
36
+
37
+ [tool.uv.sources]
38
+ proofstep-types = { workspace = true }
@@ -0,0 +1,59 @@
1
+ """Proofstep evaluation engine (pure library — no I/O).
2
+
3
+ No HTTP, no database, no provider SDKs. Model access arrives through an injected
4
+ `ModelClient` protocol. That boundary is what makes local mode, CI mode, and server
5
+ mode the same code path, and it is enforced in CI by `.importlinter`.
6
+ """
7
+
8
+ from importlib import metadata as _metadata
9
+
10
+ from proofstep_core.aggregate import aggregate_scores, scores_for
11
+ from proofstep_core.compare import Comparison, ExampleRegression, compare_metrics
12
+ from proofstep_core.dataset import Dataset
13
+ from proofstep_core.gates import GateReport, evaluate_gates
14
+ from proofstep_core.paths import PathError, resolve, resolve_in_context
15
+ from proofstep_core.runner import EvalResult, RunConfig, run_suite
16
+ from proofstep_core.suite import EvalSuite, FunctionEvaluator, evaluate
17
+ from proofstep_core.types import (
18
+ CorpusEvaluator,
19
+ EvalContext,
20
+ Evaluator,
21
+ EvaluatorBase,
22
+ Message,
23
+ ModelClient,
24
+ ModelResponse,
25
+ Task,
26
+ )
27
+
28
+ # Read from the installed distribution rather than written here twice. A hand-maintained
29
+ # copy drifts the first time a release bumps one and not the other — which it already did,
30
+ # reporting 0.1.0.dev0 from a 0.1.0 wheel.
31
+ __version__ = _metadata.version("proofstep-core")
32
+
33
+ __all__ = [
34
+ "Comparison",
35
+ "CorpusEvaluator",
36
+ "Dataset",
37
+ "EvalContext",
38
+ "EvalResult",
39
+ "EvalSuite",
40
+ "Evaluator",
41
+ "EvaluatorBase",
42
+ "ExampleRegression",
43
+ "FunctionEvaluator",
44
+ "GateReport",
45
+ "Message",
46
+ "ModelClient",
47
+ "ModelResponse",
48
+ "PathError",
49
+ "RunConfig",
50
+ "Task",
51
+ "aggregate_scores",
52
+ "compare_metrics",
53
+ "evaluate",
54
+ "evaluate_gates",
55
+ "resolve",
56
+ "resolve_in_context",
57
+ "run_suite",
58
+ "scores_for",
59
+ ]
@@ -0,0 +1,140 @@
1
+ """Roll per-example scores up into metrics.
2
+
3
+ The load-bearing rule here: **errored evaluations are excluded from the mean and
4
+ counted separately.** A judge that timed out is not a failing example, and averaging
5
+ infrastructure failures in as zeros is the fastest way to make a metric untrustworthy
6
+ (docs/EVALUATION_ENGINE.md §1).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections import defaultdict
12
+ from collections.abc import Iterable, Sequence
13
+
14
+ from proofstep_core.stats import bootstrap_ci, mean, stddev
15
+ from proofstep_types import ExampleResult, Metric, Score
16
+
17
+ # A slice is identified by its sorted key/value pairs so it can be a dict key.
18
+ SliceKey = tuple[tuple[str, str], ...]
19
+
20
+
21
+ def _slice_key(slice_: dict[str, str] | None) -> SliceKey:
22
+ return tuple(sorted(slice_.items())) if slice_ else ()
23
+
24
+
25
+ def _unkey(key: SliceKey) -> dict[str, str] | None:
26
+ return dict(key) if key else None
27
+
28
+
29
+ def aggregate_scores(
30
+ results: Iterable[ExampleResult],
31
+ *,
32
+ slice_by: Sequence[str] = (),
33
+ confidence_intervals: bool = True,
34
+ seed: int = 42,
35
+ ) -> list[Metric]:
36
+ """Aggregate every score across every result into metrics.
37
+
38
+ ``slice_by`` names metadata keys to additionally break each metric down by. This
39
+ is what surfaces a rare-class collapse that the aggregate hides: slicing
40
+ ``per_class_recall`` by ``class`` makes the unsubscribe number *visible* even
41
+ when nobody thought to gate on it.
42
+ """
43
+ buckets: dict[tuple[str, SliceKey], list[float]] = defaultdict(list)
44
+ errors: dict[tuple[str, SliceKey], int] = defaultdict(int)
45
+
46
+ for result in results:
47
+ extra = _extra_slices(result, slice_by)
48
+ for score in result.scores:
49
+ own = _slice_key(score.slice)
50
+ for slice_key in {own, *[_merge(own, s) for s in extra]}:
51
+ bucket = (score.metric, slice_key)
52
+ if score.errored:
53
+ errors[bucket] += 1
54
+ elif score.value is not None:
55
+ buckets[bucket].append(score.value)
56
+
57
+ # A metric that produced nothing but errors must still appear, with count 0 —
58
+ # otherwise a gate on it sees "metric missing" and cannot distinguish a typo
59
+ # from a wholly broken evaluator.
60
+ metrics: list[Metric] = []
61
+ for bucket in sorted(set(buckets) | set(errors)):
62
+ metric_key, slice_key = bucket
63
+ values = buckets.get(bucket, [])
64
+ error_count = errors.get(bucket, 0)
65
+ metrics.append(
66
+ _build_metric(
67
+ metric_key,
68
+ values,
69
+ error_count,
70
+ _unkey(slice_key),
71
+ confidence_intervals=confidence_intervals,
72
+ seed=seed,
73
+ )
74
+ )
75
+ return metrics
76
+
77
+
78
+ def _build_metric(
79
+ key: str,
80
+ values: list[float],
81
+ error_count: int,
82
+ slice_: dict[str, str] | None,
83
+ *,
84
+ confidence_intervals: bool,
85
+ seed: int,
86
+ ) -> Metric:
87
+ if not values:
88
+ return Metric(key=key, value=0.0, count=0, error_count=error_count, slice=slice_)
89
+
90
+ ci_low = ci_high = None
91
+ # Bootstrapping a handful of points produces an interval that says nothing;
92
+ # reporting one anyway would imply precision that isn't there.
93
+ if confidence_intervals and len(values) >= 5:
94
+ ci_low, ci_high = bootstrap_ci(values, seed=seed)
95
+
96
+ return Metric(
97
+ key=key,
98
+ value=mean(values),
99
+ count=len(values),
100
+ error_count=error_count,
101
+ stddev=stddev(values),
102
+ ci_low=ci_low,
103
+ ci_high=ci_high,
104
+ slice=slice_,
105
+ )
106
+
107
+
108
+ def _extra_slices(result: ExampleResult, slice_by: Sequence[str]) -> list[SliceKey]:
109
+ keys: list[SliceKey] = []
110
+ for dimension in slice_by:
111
+ value = result.metadata.get(dimension)
112
+ if value is None and result.expected is not None:
113
+ value = result.expected.get(dimension)
114
+ if value is not None:
115
+ keys.append(((dimension, str(value)),))
116
+ return keys
117
+
118
+
119
+ def _merge(a: SliceKey, b: SliceKey) -> SliceKey:
120
+ return tuple(sorted({**dict(a), **dict(b)}.items()))
121
+
122
+
123
+ def scores_for(results: Iterable[ExampleResult], metric_key: str) -> list[float]:
124
+ """Every non-errored value for one metric, in result order.
125
+
126
+ Used by comparison to bootstrap a CI on the delta between two runs.
127
+ """
128
+ return [
129
+ score.value
130
+ for result in results
131
+ for score in result.scores
132
+ if score.metric == metric_key and score.counts_toward_mean and score.value is not None
133
+ ]
134
+
135
+
136
+ def group_by_metric(scores: Iterable[Score]) -> dict[str, list[Score]]:
137
+ grouped: dict[str, list[Score]] = defaultdict(list)
138
+ for score in scores:
139
+ grouped[score.metric].append(score)
140
+ return dict(grouped)