oathgate 0.0.1__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.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ oath.lock.json
9
+ .pytest_cache/
10
+ .DS_Store
oathgate-0.0.1/LICENSE ADDED
@@ -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.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.5
2
+ Name: oathgate
3
+ Version: 0.0.1
4
+ License-File: LICENSE
5
+ Description-Content-Type: text/markdown
6
+
7
+ # oathgate
8
+
9
+ **Take the oath before you run the eval.**
10
+
11
+ `oathgate` blocks an evaluation run until two things are on the record:
12
+
13
+ 1. **The measurement ruler is frozen** — metrics, thresholds, dataset composition and references are hashed.
14
+ 2. **A prediction is written down** — what you expect to happen, before you know what happened.
15
+
16
+ Nothing here measures your model. It measures whether you were honest about how you measured.
17
+
18
+ ## The problem
19
+
20
+ 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.
21
+
22
+ The fix is not more rigour in the moment. It is making the ruler expensive to change *after* you have seen the outcome.
23
+
24
+ ## What gets hashed
25
+
26
+ Only the ruler:
27
+
28
+ | Hashed | Not hashed |
29
+ | --- | --- |
30
+ | `metrics` | prompt |
31
+ | `thresholds` | model / checkpoint |
32
+ | `dataset` (composition) | agent scaffold |
33
+ | `references` (gold answers) | temperature, seeds, infra |
34
+
35
+ 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.
36
+
37
+ ## Usage
38
+
39
+ Freeze the ruler and commit to a prediction:
40
+
41
+ ```console
42
+ $ oathgate freeze examples/spec.json --predict "f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar"
43
+ frozen 4f2a91c0d3e8 -> oath.lock.json
44
+ oath f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar
45
+ ```
46
+
47
+ Then, as the first step of your run:
48
+
49
+ ```console
50
+ $ oathgate check && python run_eval.py
51
+ ok 4f2a91c0d3e8 ruler unchanged
52
+ oath f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar
53
+ ```
54
+
55
+ If someone nudged a threshold in between, the gate refuses and exits non-zero:
56
+
57
+ ```console
58
+ $ oathgate check
59
+ BLOCKED: the measurement ruler changed after the oath was taken.
60
+ frozen: 4f2a91c0d3e8 (2026-08-31T09:14:00+00:00)
61
+ current: b71e05ad9c2f
62
+ Re-freeze deliberately, or restore the spec. Do not do it silently.
63
+ ```
64
+
65
+ Re-freezing is allowed. It just cannot happen by accident, and it leaves a timestamp.
66
+
67
+ ## Install
68
+
69
+ ```console
70
+ pip install oathgate
71
+ ```
72
+
73
+ ## Spec format
74
+
75
+ 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.
76
+
77
+ See [`examples/spec.json`](examples/spec.json).
78
+
79
+ ## Status
80
+
81
+ Early. The CLI is the whole surface right now; an MCP wrapper is planned so agents can be held to the same gate.
82
+
83
+ ## Licence
84
+
85
+ MIT
@@ -0,0 +1,79 @@
1
+ # oathgate
2
+
3
+ **Take the oath before you run the eval.**
4
+
5
+ `oathgate` blocks an evaluation run until two things are on the record:
6
+
7
+ 1. **The measurement ruler is frozen** — metrics, thresholds, dataset composition and references are hashed.
8
+ 2. **A prediction is written down** — what you expect to happen, before you know what happened.
9
+
10
+ Nothing here measures your model. It measures whether you were honest about how you measured.
11
+
12
+ ## The problem
13
+
14
+ 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.
15
+
16
+ The fix is not more rigour in the moment. It is making the ruler expensive to change *after* you have seen the outcome.
17
+
18
+ ## What gets hashed
19
+
20
+ Only the ruler:
21
+
22
+ | Hashed | Not hashed |
23
+ | --- | --- |
24
+ | `metrics` | prompt |
25
+ | `thresholds` | model / checkpoint |
26
+ | `dataset` (composition) | agent scaffold |
27
+ | `references` (gold answers) | temperature, seeds, infra |
28
+
29
+ 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.
30
+
31
+ ## Usage
32
+
33
+ Freeze the ruler and commit to a prediction:
34
+
35
+ ```console
36
+ $ oathgate freeze examples/spec.json --predict "f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar"
37
+ frozen 4f2a91c0d3e8 -> oath.lock.json
38
+ oath f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar
39
+ ```
40
+
41
+ Then, as the first step of your run:
42
+
43
+ ```console
44
+ $ oathgate check && python run_eval.py
45
+ ok 4f2a91c0d3e8 ruler unchanged
46
+ oath f1 lands between 0.88 and 0.92; exact_match misses the 0.80 bar
47
+ ```
48
+
49
+ If someone nudged a threshold in between, the gate refuses and exits non-zero:
50
+
51
+ ```console
52
+ $ oathgate check
53
+ BLOCKED: the measurement ruler changed after the oath was taken.
54
+ frozen: 4f2a91c0d3e8 (2026-08-31T09:14:00+00:00)
55
+ current: b71e05ad9c2f
56
+ Re-freeze deliberately, or restore the spec. Do not do it silently.
57
+ ```
58
+
59
+ Re-freezing is allowed. It just cannot happen by accident, and it leaves a timestamp.
60
+
61
+ ## Install
62
+
63
+ ```console
64
+ pip install oathgate
65
+ ```
66
+
67
+ ## Spec format
68
+
69
+ 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.
70
+
71
+ See [`examples/spec.json`](examples/spec.json).
72
+
73
+ ## Status
74
+
75
+ Early. The CLI is the whole surface right now; an MCP wrapper is planned so agents can be held to the same gate.
76
+
77
+ ## Licence
78
+
79
+ MIT
@@ -0,0 +1,3 @@
1
+ {"id": "1", "input": "Ship it by Friday, and drop the dark mode toggle.", "expected": ["deadline:friday", "remove:dark-mode-toggle"]}
2
+ {"id": "2", "input": "Can we make the export button blue?", "expected": ["change:export-button-colour"]}
3
+ {"id": "3", "input": "Nothing to change on my side.", "expected": []}
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "synthetic-extraction-eval",
3
+ "metrics": [
4
+ "exact_match",
5
+ "f1"
6
+ ],
7
+ "thresholds": {
8
+ "exact_match": 0.8,
9
+ "f1": 0.9
10
+ },
11
+ "dataset": {
12
+ "path": "examples/dataset.jsonl",
13
+ "n": 3
14
+ },
15
+ "references": "examples/dataset.jsonl",
16
+ "system": {
17
+ "_comment": "Everything under `system` is intentionally NOT hashed.",
18
+ "model": "another-model:latest",
19
+ "prompt": "prompts/extract.md",
20
+ "temperature": 0.0
21
+ }
22
+ }
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "oathgate"
3
+ version = "0.0.1"
4
+ readme = "README.md"
5
+
6
+ [project.scripts]
7
+ oathgate = "oathgate.cli:main"
8
+
9
+ [tool.hatch.build.targets.wheel]
10
+ packages = ["src/oathgate"]
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
@@ -0,0 +1,3 @@
1
+ """oathgate — freeze the measurement ruler before you run the eval."""
2
+
3
+ __version__ = "0.0.1"
@@ -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())