oathgate 0.0.2__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.
oathgate/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """oathgate — freeze the measurement ruler before you run the eval."""
2
+
3
+ __version__ = "0.0.1"
oathgate/cli.py ADDED
@@ -0,0 +1,107 @@
1
+ """oathgate — freeze the measurement ruler before you run the eval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import sys
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ # Only these keys describe the *ruler*: how a result is measured.
13
+ # The system under test (prompt, model, agent, temperature) is deliberately
14
+ # excluded — you are allowed to change it, that is the whole point.
15
+ RULER_KEYS = ("metrics", "thresholds", "dataset", "references")
16
+
17
+ LOCK_NAME = "oath.lock.json"
18
+
19
+
20
+ def _canonical(spec: dict) -> str:
21
+ ruler = {k: spec[k] for k in RULER_KEYS if k in spec}
22
+ return json.dumps(ruler, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
23
+
24
+
25
+ def _digest(spec: dict) -> str:
26
+ return hashlib.sha256(_canonical(spec).encode("utf-8")).hexdigest()
27
+
28
+
29
+ def _load(path: Path) -> dict:
30
+ if not path.exists():
31
+ sys.exit(f"oathgate: spec not found: {path}")
32
+ try:
33
+ return json.loads(path.read_text(encoding="utf-8"))
34
+ except json.JSONDecodeError as exc:
35
+ sys.exit(f"oathgate: cannot parse {path}: {exc}")
36
+
37
+
38
+ def cmd_freeze(args: argparse.Namespace) -> int:
39
+ spec_path = Path(args.spec)
40
+ spec = _load(spec_path)
41
+
42
+ missing = [k for k in RULER_KEYS if k not in spec]
43
+ if missing:
44
+ sys.exit(f"oathgate: spec is missing required keys: {', '.join(missing)}")
45
+
46
+ if not args.predict:
47
+ sys.exit("oathgate: refusing to freeze without a prediction (--predict)")
48
+
49
+ lock = {
50
+ "version": 1,
51
+ "spec": str(spec_path),
52
+ "ruler_sha256": _digest(spec),
53
+ "prediction": args.predict,
54
+ "frozen_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
55
+ }
56
+ lock_path = Path(args.lock)
57
+ lock_path.write_text(json.dumps(lock, indent=2) + "\n", encoding="utf-8")
58
+ print(f"frozen {lock['ruler_sha256'][:12]} -> {lock_path}")
59
+ print(f"oath {args.predict}")
60
+ return 0
61
+
62
+
63
+ def cmd_check(args: argparse.Namespace) -> int:
64
+ lock_path = Path(args.lock)
65
+ if not lock_path.exists():
66
+ sys.exit(f"oathgate: no lock file ({lock_path}). Run `oathgate freeze` first.")
67
+
68
+ lock = json.loads(lock_path.read_text(encoding="utf-8"))
69
+ spec = _load(Path(args.spec or lock["spec"]))
70
+ current = _digest(spec)
71
+
72
+ if current != lock["ruler_sha256"]:
73
+ print("BLOCKED: the measurement ruler changed after the oath was taken.")
74
+ print(f" frozen: {lock['ruler_sha256'][:12]} ({lock['frozen_at']})")
75
+ print(f" current: {current[:12]}")
76
+ print(" Re-freeze deliberately, or restore the spec. Do not do it silently.")
77
+ return 1
78
+
79
+ print(f"ok {current[:12]} ruler unchanged")
80
+ print(f"oath {lock['prediction']}")
81
+ return 0
82
+
83
+
84
+ def main(argv: list[str] | None = None) -> int:
85
+ parser = argparse.ArgumentParser(
86
+ prog="oathgate",
87
+ description="Freeze the eval scoring spec and your prediction before the run.",
88
+ )
89
+ sub = parser.add_subparsers(dest="command", required=True)
90
+
91
+ f = sub.add_parser("freeze", help="freeze the ruler and record a prediction")
92
+ f.add_argument("spec", help="path to the eval spec (JSON)")
93
+ f.add_argument("--predict", required=True, help="what you expect to happen")
94
+ f.add_argument("--lock", default=LOCK_NAME, help=f"lock file (default: {LOCK_NAME})")
95
+ f.set_defaults(func=cmd_freeze)
96
+
97
+ c = sub.add_parser("check", help="verify the ruler is unchanged before a run")
98
+ c.add_argument("spec", nargs="?", help="path to the eval spec (default: from lock)")
99
+ c.add_argument("--lock", default=LOCK_NAME, help=f"lock file (default: {LOCK_NAME})")
100
+ c.set_defaults(func=cmd_check)
101
+
102
+ args = parser.parse_args(argv)
103
+ return args.func(args)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ raise SystemExit(main())
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.5
2
+ Name: oathgate
3
+ Version: 0.0.2
4
+ Summary: Freeze the eval scoring spec and your prediction before the run.
5
+ Project-URL: Homepage, https://github.com/UladzKha/oathgate
6
+ Project-URL: Issues, https://github.com/UladzKha/oathgate/issues
7
+ Author: Uladzimir Khadakouski
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: benchmark,evals,evaluation,llm,reproducibility
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+
19
+ # oathgate
20
+
21
+ **Take the oath before you run the eval.**
22
+
23
+ `oathgate` blocks an evaluation run until two things are on the record:
24
+
25
+ 1. **The measurement ruler is frozen** — metrics, thresholds, dataset composition and references are hashed.
26
+ 2. **A prediction is written down** — what you expect to happen, before you know what happened.
27
+
28
+ Nothing here measures your model. It measures whether you were honest about how you measured.
29
+
30
+ ## The problem
31
+
32
+ Evals drift. Not because anyone lies, but because the ruler is soft: a threshold moves from 0.85 to 0.80, three hard cases quietly leave the set, a metric is swapped for a friendlier one — and the number goes up. Every step is defensible on its own. The result is a benchmark that only ever improves.
33
+
34
+ The fix is not more rigour in the moment. It is making the ruler expensive to change _after_ you have seen the outcome.
35
+
36
+ ## What gets hashed
37
+
38
+ Only the ruler:
39
+
40
+ | Hashed | Not hashed |
41
+ | --------------------------- | ------------------------- |
42
+ | `metrics` | prompt |
43
+ | `thresholds` | model / checkpoint |
44
+ | `dataset` (composition) | agent scaffold |
45
+ | `references` (gold answers) | temperature, seeds, infra |
46
+
47
+ This split is the whole design. You are _supposed_ to change the system under test — that is the experiment. The ruler is what has to hold still for the comparison to mean anything.
48
+
49
+ ## Usage
50
+
51
+ Freeze the ruler and commit to a prediction:
52
+
53
+ ```console
54
+ $ oathgate freeze examples/spec.json --predict "f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar"
55
+ frozen 4f2a91c0d3e8 -> oath.lock.json
56
+ oath f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar
57
+ ```
58
+
59
+ Then, as the first step of your run:
60
+
61
+ ```console
62
+ $ oathgate check && python run_eval.py
63
+ ok 4f2a91c0d3e8 ruler unchanged
64
+ oath f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar
65
+ ```
66
+
67
+ If someone nudged a threshold in between, the gate refuses and exits non-zero:
68
+
69
+ ```console
70
+ $ oathgate check
71
+ BLOCKED: the measurement ruler changed after the oath was taken.
72
+ frozen: 4f2a91c0d3e8 (2026-08-31T09:14:00+00:00)
73
+ current: b71e05ad9c2f
74
+ Re-freeze deliberately, or restore the spec. Do not do it silently.
75
+ ```
76
+
77
+ Re-freezing is allowed. It just cannot happen by accident, and it leaves a timestamp.
78
+
79
+ ## Install
80
+
81
+ ```console
82
+ pip install oathgate
83
+ ```
84
+
85
+ ## Spec format
86
+
87
+ A JSON file with the four ruler keys. Anything else — including a `system` block describing what you are testing — is ignored by the hash and free to change.
88
+
89
+ See [`examples/spec.json`](https://github.com/UladzKha/oathgate/blob/main/examples/spec.json).
90
+
91
+ ## Status
92
+
93
+ Early. The CLI is the whole surface right now; an MCP wrapper is planned so agents can be held to the same gate.
94
+
95
+ ## Licence
96
+
97
+ MIT
@@ -0,0 +1,7 @@
1
+ oathgate/__init__.py,sha256=zrLovLYUKPjjJamxwtNiO5MfPzvNTPKtJ0b-t1S5sfY,95
2
+ oathgate/cli.py,sha256=Ce8SxpO_CLSGTRTMZNEE0B2cVoyyGxk8l1ZS6Xh2Q8s,3779
3
+ oathgate-0.0.2.dist-info/METADATA,sha256=3e9d-OeFXdh3NlmAF2SPUfc7K8-LyivLFCABzdjLQ1Y,3514
4
+ oathgate-0.0.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
5
+ oathgate-0.0.2.dist-info/entry_points.txt,sha256=380Z8qNh4DjznwbDwJ2TgDtXh7ndp_JQVwdGP1QLUMQ,47
6
+ oathgate-0.0.2.dist-info/licenses/LICENSE,sha256=hL3Xx-WseS6-fKaqSCbFcvdKOt2cZ4N1bnfs1HrtCHY,1093
7
+ oathgate-0.0.2.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
+ oathgate = oathgate.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Uladzimir Khadakouski
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 Uladzimir KhadakouskiS 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.