proofstep-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,343 @@
1
+ """The suite YAML schema.
2
+
3
+ Versioned by `apiVersion` from the first release. Suite files live in users' repos
4
+ and must not break on upgrade, which costs nothing to promise now and is expensive
5
+ to retrofit later (docs/OPEN_QUESTIONS.md Q11).
6
+
7
+ `extra="forbid"` throughout: a typo'd key that is silently ignored produces a suite
8
+ which looks configured and is not. `capture_args: true` misspelled as `capture_arg`
9
+ should be an error, not a shrug.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import difflib
15
+ from typing import Any, Literal
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
18
+
19
+ API_VERSION = "proofstep.dev/v1"
20
+
21
+ ReportFormat = Literal["terminal", "json"]
22
+
23
+ EVALUATOR_TYPES = (
24
+ "exact_match",
25
+ "json_schema",
26
+ "regex",
27
+ "contains",
28
+ "length",
29
+ "numeric_range",
30
+ "set_comparison",
31
+ "llm_judge",
32
+ "trajectory",
33
+ "operational",
34
+ "classification",
35
+ "ranking",
36
+ "calibration",
37
+ "discrimination",
38
+ )
39
+
40
+
41
+ class DatasetRef(BaseModel):
42
+ model_config = ConfigDict(extra="forbid")
43
+
44
+ name: str | None = None
45
+ version: str = "latest-locked"
46
+ path: str | None = Field(
47
+ default=None, description="Local JSONL/CSV file; mutually exclusive with `name`"
48
+ )
49
+ split: str | None = None
50
+ limit: int | None = Field(default=None, ge=1)
51
+
52
+ @model_validator(mode="after")
53
+ def _one_source(self) -> DatasetRef:
54
+ if bool(self.name) == bool(self.path):
55
+ msg = "dataset needs exactly one of `name` (server) or `path` (local file)"
56
+ raise ValueError(msg)
57
+ return self
58
+
59
+ @property
60
+ def is_local(self) -> bool:
61
+ return self.path is not None
62
+
63
+
64
+ class TaskRef(BaseModel):
65
+ model_config = ConfigDict(extra="forbid")
66
+
67
+ entrypoint: str = Field(pattern=r"^[\w.]+:[\w.]+$", description="module:function")
68
+ timeout_s: float = Field(default=120.0, gt=0)
69
+ retries: int = Field(default=2, ge=0)
70
+
71
+
72
+ class Execution(BaseModel):
73
+ model_config = ConfigDict(extra="forbid")
74
+
75
+ concurrency: int = Field(default=8, ge=1, le=256)
76
+ judge_concurrency: int = Field(default=4, ge=1, le=64)
77
+ max_error_rate: float = Field(default=0.10, ge=0, le=1)
78
+ seed: int = 42
79
+ slice_by: list[str] = Field(default_factory=list)
80
+ max_cost: float | None = Field(default=None, gt=0)
81
+
82
+
83
+ class Scale(BaseModel):
84
+ model_config = ConfigDict(extra="forbid")
85
+
86
+ min: int = 1
87
+ max: int = 5
88
+ normalize: bool = True
89
+
90
+
91
+ class Calibration(BaseModel):
92
+ """One judge's calibration: which labelled set, and what it has to achieve.
93
+
94
+ Thresholds live with the evaluator rather than with the gate because they are a
95
+ property of the measurement — an unsubscribe judge needs a tighter false-pass rate
96
+ than a tone judge, whatever gate set happens to reference it. Whether falling short
97
+ *blocks* is the gate set's decision (`calibration.require`).
98
+ """
99
+
100
+ model_config = ConfigDict(extra="forbid")
101
+
102
+ #: Path to the labelled JSONL, relative to the suite file.
103
+ dataset: str
104
+ version: str = "latest-locked"
105
+ #: Labels that count as "passing", which is what makes the false-pass and false-fail
106
+ #: rates computable. Without it those two numbers are unmeasured, and they are the
107
+ #: ones that matter most.
108
+ passing_labels: list[str] = Field(default_factory=list)
109
+ min_agreement: float | None = Field(default=None, ge=0, le=1)
110
+ min_kappa: float | None = Field(default=None, ge=-1, le=1)
111
+ max_false_pass_rate: float | None = Field(default=None, ge=0, le=1)
112
+ max_false_fail_rate: float | None = Field(default=None, ge=0, le=1)
113
+ min_examples: int | None = Field(default=None, ge=1)
114
+ min_per_class: int | None = Field(default=None, ge=1)
115
+ allow_position_bias: bool = False
116
+ required: bool = False
117
+
118
+
119
+ class CalibrationPolicy(BaseModel):
120
+ """Suite-level calibration settings.
121
+
122
+ `require` is the enforcement switch: `false` means an uncalibrated gated judge only
123
+ warns, `true` means it fails the run, and a mapping overrides the thresholds that
124
+ apply when an evaluator does not state its own.
125
+ """
126
+
127
+ model_config = ConfigDict(extra="forbid")
128
+
129
+ #: Where calibration records live, relative to the suite file. They are committed to
130
+ #: git so `require` works in CI with no server — see calibration_store.py.
131
+ directory: str = "calibrations"
132
+ require: bool | dict[str, Any] = False
133
+
134
+
135
+ class EvaluatorSpec(BaseModel):
136
+ model_config = ConfigDict(extra="forbid")
137
+
138
+ name: str = Field(pattern=r"^[a-z][a-z0-9_]*$")
139
+ type: str
140
+
141
+ # Deterministic
142
+ field: str | None = None
143
+ expected_field: str | None = None
144
+ normalize: Literal["none", "case", "whitespace", "punctuation", "all"] = "none"
145
+ schema_: dict[str, Any] | None = Field(default=None, alias="schema")
146
+ schema_path: str | None = None
147
+ allow: list[str] = Field(default_factory=list)
148
+ deny: list[str] = Field(default_factory=list)
149
+ substrings: list[str] = Field(default_factory=list)
150
+ mode: str | None = None
151
+ case_sensitive: bool = True
152
+ unit: Literal["chars", "words"] = "chars"
153
+ minimum: float | None = None
154
+ maximum: float | None = None
155
+ inclusive: bool = True
156
+
157
+ # Judge
158
+ rubric: str | None = None
159
+ rubric_path: str | None = None
160
+ model: str | None = None
161
+ temperature: float = 0.0
162
+ seed: int | None = 42
163
+ inputs: list[str] = Field(default_factory=list)
164
+ labels: list[str] = Field(default_factory=list)
165
+ #: Which of `labels` count as a pass, turning a classify judge's label into a gateable rate.
166
+ #: Declared on the evaluator rather than only under `calibration`, because it changes what the
167
+ #: judge scores and not merely how it is calibrated.
168
+ passing_labels: list[str] = Field(default_factory=list)
169
+ scale: Scale = Field(default_factory=Scale)
170
+ votes: int = 1
171
+ timeout_s: float = 60.0
172
+ max_retries: int = 2
173
+ calibration: Calibration | None = None
174
+
175
+ # Trajectory
176
+ policy: str | None = None
177
+
178
+ # Statistical / operational
179
+ prediction_field: str | None = None
180
+ label_field: str | None = None
181
+ averaging: Literal["macro", "micro", "weighted"] = "macro"
182
+ k: int = 10
183
+ ranking_field: str | None = None
184
+ relevant_field: str | None = None
185
+ confidence_field: str | None = None
186
+ #: A boolean output field saying whether the prediction was right, for cases where
187
+ #: correctness is not a prediction/label comparison.
188
+ correct_field: str | None = None
189
+ percentiles: list[int] = Field(default_factory=lambda: [50, 95, 99])
190
+
191
+ @model_validator(mode="after")
192
+ def _known_type(self) -> EvaluatorSpec:
193
+ if self.type not in EVALUATOR_TYPES:
194
+ closest = _closest(self.type, EVALUATOR_TYPES)
195
+ hint = f" Did you mean {closest!r}?" if closest else ""
196
+ msg = (
197
+ f"evaluator {self.name!r} has unknown type {self.type!r}.{hint} "
198
+ f"Valid types: {', '.join(sorted(EVALUATOR_TYPES))}"
199
+ )
200
+ raise ValueError(msg)
201
+ return self
202
+
203
+ @model_validator(mode="after")
204
+ def _judges_declare_inputs(self) -> EvaluatorSpec:
205
+ if self.type != "llm_judge":
206
+ return self
207
+ if not self.inputs:
208
+ # A judge handed the whole example can read `expected` and grade against
209
+ # the answer key. Enumerating inputs is the only thing that prevents it.
210
+ msg = (
211
+ f"judge {self.name!r} must declare `inputs`: without it the judge can see "
212
+ "`expected` and grade against the answer key"
213
+ )
214
+ raise ValueError(msg)
215
+ if not self.rubric and not self.rubric_path:
216
+ msg = f"judge {self.name!r} needs `rubric` or `rubric_path`"
217
+ raise ValueError(msg)
218
+ if not self.model:
219
+ # Unpinned means the provider can change the model underneath you and
220
+ # invalidate every historical number without any signal.
221
+ msg = (
222
+ f"judge {self.name!r} must pin a `model`: an unpinned judge silently "
223
+ "invalidates every historical comparison when the provider updates it"
224
+ )
225
+ raise ValueError(msg)
226
+ return self
227
+
228
+ @model_validator(mode="after")
229
+ def _trajectory_needs_a_policy(self) -> EvaluatorSpec:
230
+ if self.type == "trajectory" and not self.policy:
231
+ msg = f"trajectory evaluator {self.name!r} needs a `policy` path"
232
+ raise ValueError(msg)
233
+ return self
234
+
235
+
236
+ class GateSpec(BaseModel):
237
+ model_config = ConfigDict(extra="forbid")
238
+
239
+ minimum: float | None = None
240
+ maximum: float | None = None
241
+ max_regression: float | None = None
242
+ max_relative_regression: float | None = None
243
+ blocking: bool = True
244
+ slice: dict[str, str] | None = None
245
+ require_baseline: bool = False
246
+ #: Alpha for a paired significance test. With it, a regression must be both bigger than the
247
+ #: threshold *and* distinguishable from noise before it fails the build — because at the sample
248
+ #: sizes eval suites actually run at, most measured regressions are neither.
249
+ significance: float | None = Field(default=None, ge=0.0, le=1.0)
250
+ #: Report ERROR when the run was too small to have detected the regression this gate guards.
251
+ #: A green check from a test that could never have failed is worse than no check.
252
+ require_power: bool = False
253
+
254
+ @model_validator(mode="after")
255
+ def _has_a_condition(self) -> GateSpec:
256
+ if all(
257
+ v is None
258
+ for v in (
259
+ self.minimum,
260
+ self.maximum,
261
+ self.max_regression,
262
+ self.max_relative_regression,
263
+ )
264
+ ):
265
+ msg = "declares no condition; a gate that cannot fail gives false assurance"
266
+ raise ValueError(msg)
267
+ return self
268
+
269
+
270
+ class BaselineSpec(BaseModel):
271
+ model_config = ConfigDict(extra="forbid")
272
+
273
+ strategy: Literal["latest_on_branch", "run_id", "none"] = "latest_on_branch"
274
+ branch: str = "main"
275
+ run_id: str | None = None
276
+ require_dataset_match: bool = True
277
+
278
+ @model_validator(mode="after")
279
+ def _run_id_present(self) -> BaselineSpec:
280
+ if self.strategy == "run_id" and not self.run_id:
281
+ msg = "baseline strategy 'run_id' needs a `run_id`"
282
+ raise ValueError(msg)
283
+ return self
284
+
285
+
286
+ class ReportSpec(BaseModel):
287
+ model_config = ConfigDict(extra="forbid")
288
+
289
+ formats: list[ReportFormat] = Field(
290
+ default_factory=lambda: ["terminal", "json"] # type: ignore[arg-type]
291
+ )
292
+ output: str = "proofstep-report.json"
293
+
294
+
295
+ class Suite(BaseModel):
296
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
297
+
298
+ apiVersion: str = API_VERSION # noqa: N815 — YAML field name
299
+ kind: Literal["EvalSuite"] = "EvalSuite"
300
+ name: str = Field(pattern=r"^[a-z][a-z0-9-]*$")
301
+ description: str | None = None
302
+ extends: str | None = None
303
+
304
+ dataset: DatasetRef
305
+ task: TaskRef | None = None
306
+ configuration: dict[str, Any] = Field(default_factory=dict)
307
+ execution: Execution = Field(default_factory=Execution)
308
+ evaluators: list[EvaluatorSpec] = Field(min_length=1)
309
+ gates: dict[str, GateSpec] = Field(default_factory=dict)
310
+ calibration: CalibrationPolicy = Field(default_factory=CalibrationPolicy)
311
+ baseline: BaselineSpec = Field(default_factory=BaselineSpec)
312
+ report: ReportSpec = Field(default_factory=ReportSpec)
313
+
314
+ @model_validator(mode="after")
315
+ def _supported_api_version(self) -> Suite:
316
+ if self.apiVersion != API_VERSION:
317
+ msg = (
318
+ f"unsupported apiVersion {self.apiVersion!r}; this CLI understands {API_VERSION!r}"
319
+ )
320
+ raise ValueError(msg)
321
+ return self
322
+
323
+ @model_validator(mode="after")
324
+ def _unique_evaluator_names(self) -> Suite:
325
+ seen: set[str] = set()
326
+ for evaluator in self.evaluators:
327
+ if evaluator.name in seen:
328
+ msg = f"duplicate evaluator name {evaluator.name!r}"
329
+ raise ValueError(msg)
330
+ seen.add(evaluator.name)
331
+ return self
332
+
333
+ @property
334
+ def judge_ratio(self) -> float:
335
+ if not self.evaluators:
336
+ return 0.0
337
+ judges = sum(1 for e in self.evaluators if e.type == "llm_judge")
338
+ return judges / len(self.evaluators)
339
+
340
+
341
+ def _closest(value: str, candidates: tuple[str, ...]) -> str | None:
342
+ matches = difflib.get_close_matches(value, candidates, n=1, cutoff=0.6)
343
+ return matches[0] if matches else None
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.5
2
+ Name: proofstep-cli
3
+ Version: 0.1.0
4
+ Summary: Proofstep command-line interface — run suites, gate CI, compare experiments
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: httpx>=0.27
20
+ Requires-Dist: proofstep
21
+ Requires-Dist: proofstep-core
22
+ Requires-Dist: proofstep-trajectory
23
+ Requires-Dist: proofstep-types
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: rich>=13.9
26
+ Requires-Dist: typer>=0.15
27
+ Description-Content-Type: text/markdown
28
+
29
+ # proofstep-cli
30
+
31
+ **The `proofstep` command** — part of [Proofstep](https://github.com/IlaKhan17/proofstep), the CI gate for AI
32
+ agents that knows the difference between a regression and a bad day.
33
+
34
+ Run an evaluation suite, apply its gates, and exit non-zero when a protected metric regresses.
35
+
36
+ ```bash
37
+ proofstep eval evals/suites/reply-intent.yaml
38
+ echo $? # 0 merge · 1 a blocking gate failed · 2 execution error · 3 the suite is wrong
39
+ ```
40
+
41
+ The exit code is the contract. Everything else — the terminal table, the JSON report, the
42
+ pull-request comment — exists to explain it.
43
+
44
+ Gates can be statistically honest rather than just thresholded:
45
+
46
+ ```yaml
47
+ gates:
48
+ intent_accuracy:
49
+ max_regression: 0.02
50
+ significance: 0.05 # only fail if the drop is distinguishable from noise
51
+ require_power: true # and ERROR if this run could never have detected it
52
+ ```
53
+
54
+ ## Documentation
55
+
56
+ Full documentation lives in the [repository](https://github.com/IlaKhan17/proofstep/tree/main/docs).
57
+
58
+ Apache-2.0.
@@ -0,0 +1,21 @@
1
+ proofstep_cli/__init__.py,sha256=kZDCFB-6Wudptp_a3AK2xCFx-s2muRDAL_oxo_J6BsI,341
2
+ proofstep_cli/calibration.py,sha256=3_iQ7aNBY3o0yYOe2ejS8xfvel_tfFgovoOm6AEoiMc,11572
3
+ proofstep_cli/calibration_store.py,sha256=wjxopf2nAl1wKOXAfkivjD6u2E7PhIoTmLloFfZoDHw,7946
4
+ proofstep_cli/main.py,sha256=4wtRYvoWWpO8TWOC9E4pvtiDX5JtzoQzymHuW5I5rjQ,21821
5
+ proofstep_cli/publish.py,sha256=5XZ-eg40uoUXYXOx5qCzT0nfIi2KofT7f2XmJTwb-Qk,22955
6
+ proofstep_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ proofstep_cli/registry.py,sha256=8X8IL8U_8tTCthI5pycy_IUxgGpddTK649S9XerLcyk,7951
8
+ proofstep_cli/runner.py,sha256=8MYXZTXCxFyXObE2mWZEBLzgD_IBvyB4qVrkpj92UgY,12651
9
+ proofstep_cli/commands/__init__.py,sha256=QN-uD3qaiBO7K7E4QEfRPnoh3rvXcIe89veVtfE8c38,23
10
+ proofstep_cli/render/__init__.py,sha256=bmQ3dGBiHqSPwumamEebbTyZSdWgUDms_jPjo1MBB0Y,24
11
+ proofstep_cli/render/calibration.py,sha256=r9Zidu3Dw4AMe7aOj24DayDGJmiQJnQIKvZhudPXa8o,5108
12
+ proofstep_cli/render/markdown.py,sha256=Y58yri1ynyD47vE0VTAgMBVqdIYyzVMjrZ1rSnRBYlk,10651
13
+ proofstep_cli/render/report.py,sha256=44BKaBO5Li42t1XzEGYqnXPoKCIgThGipyUyPBlw5Jc,6708
14
+ proofstep_cli/render/terminal.py,sha256=Ax2PBj1gtWExN6yS5IQpo8BFUy_-uQSYmZDrv65BNGI,10343
15
+ proofstep_cli/suite/__init__.py,sha256=CEqbhLYlg7SJoIKNYdV4W5m7MrwQa21mG7OhK5TGNAM,27
16
+ proofstep_cli/suite/loader.py,sha256=llGUMwSt5LEB__hz3Pv7SpmHt1d0Qqj1cXGSrRi3VN4,14050
17
+ proofstep_cli/suite/schema.py,sha256=p6iL4Mgz9baqqjVz9TtpHGgLgDtb3RrmbOBlw1Ji0Ow,12459
18
+ proofstep_cli-0.1.0.dist-info/METADATA,sha256=3ueFoVrFRD0nygOEwLm5EcbgvjoJEy71zcDrHOe1nmQ,2148
19
+ proofstep_cli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
20
+ proofstep_cli-0.1.0.dist-info/entry_points.txt,sha256=kvjBHqD1IOyphRKMklvkz_Yrhe4VxUY5bZkuK35yrNY,53
21
+ proofstep_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ proofstep = proofstep_cli.main:app