gameability 0.2.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Empyrean Intelligence
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,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: gameability
3
+ Version: 0.2.0
4
+ Summary: Find the problems in a code benchmark that accept broken solutions.
5
+ Author: Empyrean Intelligence
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Empyrean-Intelligence/gameability
8
+ Project-URL: Source, https://github.com/Empyrean-Intelligence/gameability
9
+ Project-URL: Issues, https://github.com/Empyrean-Intelligence/gameability/issues
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: hf
17
+ Requires-Dist: datasets; extra == "hf"
18
+ Dynamic: license-file
19
+
20
+ # gameability
21
+
22
+ **Find the problems in a code benchmark that accept broken solutions.**
23
+
24
+ When a model learns from tests, the tests are its reward. A self-improving system trained on tests that
25
+ accept broken code learns to pass weak tests, not to write correct code. `gameability` measures how often
26
+ that can happen. It is part of the verification layer we use at Empyrean Intelligence for self-improvement
27
+ work, released for anyone who builds benchmarks or RL training sets.
28
+
29
+ A benchmark score means something only if the benchmark rejects solutions that lack the
30
+ skill it measures. `gameability` checks that directly. It takes each problem's reference
31
+ solution, breaks it in one small place, confirms the result really is wrong, and then
32
+ asks whether the problem's own tests notice.
33
+
34
+ ```
35
+ pip install "gameability[hf]"
36
+ gameability check mbpp
37
+ ```
38
+
39
+ On MBPP, **17.9% of problems (169 of 942 judged) accept a broken solution**; on HumanEval,
40
+ **10.3% (16 of 155)**. Each run takes minutes on a laptop, with no model and no API key.
41
+
42
+ **Confirmed on valid inputs.** On the problems EvalPlus also covers, every accepted broken solution
43
+ was re-run against EvalPlus's independent tests, whose inputs respect each problem's rules:
44
+ **95.2% of flagged MBPP problems and 84.6% of flagged HumanEval problems are confirmed** (the broken
45
+ solution passes the original tests and fails EvalPlus). The two unconfirmed HumanEval problems
46
+ (116 and 129) are ones whose own visible tests contradict their written specification.
47
+
48
+ **What changed in v0.2.** v0.1 built fresh inputs by changing values freely, which sometimes asked
49
+ a problem about inputs it never promised to handle (an unsorted list for binary search, random
50
+ letters for a parenthesis parser). v0.2 learns each argument's domain from the problem's own
51
+ visible tests and stays inside it: number signs and minimums, the characters or whole tokens a
52
+ string uses, and whether lists are sorted, unique, rectangular or a permutation. On the same
53
+ problems this raised the confirmed share from 85.7% to 95.2% (MBPP) and 78.6% to 84.6% (HumanEval)
54
+ while keeping 93.1% of v0.1's confirmed detections. `--inputs free` reproduces v0.1.
55
+
56
+ ## Scoreboard
57
+
58
+ Problems that accept at least one broken solution (wrong on at least one fresh input), with 95% intervals. Defaults (24 broken
59
+ versions per problem, 8 fresh inputs, seed 0), laptop CPU, October 2026.
60
+
61
+ | Benchmark | Weak problems | Command |
62
+ |---|---|---|
63
+ | MBPP | 17.9% (169 of 942) [15.6, 20.5] | `gameability check mbpp` |
64
+ | MBPP sanitized | 13.8% (57 of 413) [10.8, 17.5] | `gameability check mbpp-sanitized` |
65
+ | HumanEval | 10.3% (16 of 155) [6.5, 16.1] | `gameability check humaneval` |
66
+ | KodCode (RL training set, 1,000-row random sample) | 9.7% (45 of 466) [7.3, 12.7] | `gameability check kodcode:rows.jsonl` |
67
+ | HumanEval+ (EvalPlus) | 3.4% (5 of 147) [1.5, 7.7] | `gameability check humaneval-plus` |
68
+ | MBPP+ (EvalPlus) | 2.0% (7 of 353) [1.0, 4.0] | `gameability check mbpp-plus` |
69
+
70
+ EvalPlus's stronger tests look like a large improvement, but EvalPlus also rewrote most reference
71
+ solutions. Holding the original reference fixed (`mbpp-plus-origref`, `humaneval-plus-origref`), the weak
72
+ share roughly halves (MBPP 8.7% to 3.1% on 254 problems, HumanEval 10.6% to 4.9% on 123), and at these
73
+ sample sizes the intervals overlap. KodCode's judged set is the problems whose tests call the function with
74
+ literal arguments (466 of 801 loadable). `rows.jsonl` is KodCode rows saved as JSON lines; the sample above
75
+ is reproducible with `python tests/sample_kodcode.py rows.jsonl` (positions from `random.Random(0)`).
76
+
77
+ ## What it prints
78
+
79
+ ```
80
+ 415 -- 6 of 11 broken solutions pass every visible test
81
+ - if arr_len < 2:
82
+ + if arr_len <= 2:
83
+ a test that catches it: assert max_Product([4, 9]) == (4, 9)
84
+ ```
85
+
86
+ For every weak problem you get the one line that was changed, how many broken versions got
87
+ through, and an assert that would have caught one.
88
+
89
+ **`--fixes fixes.py` writes those asserts to a file; review them before adding them.** Measured on
90
+ 34 MBPP/HumanEval problems against a 1.5B model's real solutions: v0.2's suggested asserts wrongly
91
+ reject **2.6% of its correct solutions** (8 of 307; v0.1: 15.3%). They target the broken versions
92
+ gameability builds, not the mistakes a particular model makes: they caught 4 of that model's 24
93
+ real wrong solutions. Treat them as leads, not as a test suite.
94
+
95
+ ## How it works
96
+
97
+ For each problem the benchmark supplies (reference solution, visible tests):
98
+
99
+ 1. The reference must pass its own visible tests. If it doesn't, the problem is excluded and counted.
100
+ 2. Fresh inputs are built by varying the arguments in the visible tests. The reference's
101
+ outputs on them are the hidden answer key.
102
+ 3. The reference is mutated at a single site: a comparison flipped, an operator swapped, a
103
+ constant nudged, a `return` replaced with `return None`.
104
+ 4. A mutant counts as **broken** only if it gives a different answer from the reference on
105
+ at least one fresh input. Mutants that behave identically are thrown away, so they can't
106
+ inflate the number.
107
+ 5. A broken mutant that still passes every visible test is a **survivor**. A problem with at
108
+ least one survivor accepts a wrong solution.
109
+
110
+ Benchmark code runs in a child process with a step budget, so a runaway mutant is stopped at
111
+ the same point on any machine; a wall-clock limit is only a backstop. Mutants that never
112
+ finish are reported as truncated, never counted as rejected.
113
+
114
+ ## Your own benchmark
115
+
116
+ One problem per line in a `.jsonl` file:
117
+
118
+ ```json
119
+ {"id": "clamp", "reference": "def clamp(x, lo, hi): ...", "tests": ["assert clamp(5, 0, 10) == 5"]}
120
+ ```
121
+
122
+ Optional fields: `entry` (the function name, if it can't be inferred), `setup` (code run
123
+ before the tests), `hidden` (held-out asserts you already have; they are used as extra
124
+ answer-key inputs), `call_alias` (the name the tests call the solution by, e.g. `candidate`).
125
+
126
+ ```
127
+ gameability check examples/tiny.jsonl --fixes fixes.py --json report.json
128
+ ```
129
+
130
+ ## What the number means, and what it doesn't
131
+
132
+ - **The attacker is weak.** One small edit is the simplest possible way to be wrong; a model trained
133
+ against these tests can search far more. In that sense the rates are a floor.
134
+ - **Fresh inputs can still break a problem's rules in ways the tests don't reveal.** v0.2 keeps each
135
+ argument inside its observed domain, but it does not model relationships BETWEEN arguments (a
136
+ length argument that must equal a list's length) or rules that only the docstring states. On the
137
+ problems EvalPlus covers, 4.8% of flagged MBPP and 15.4% of flagged HumanEval problems are still
138
+ unconfirmed (v0.1: 14.3% and 21.4%).
139
+ - **It does not predict where a model will cheat.** On a 1.5B coder, false passes were no more
140
+ common on flagged problems than on others (8.4% vs 10.0%). It audits the tests' strength, not a
141
+ model's behaviour.
142
+ - **It audits the tests, not any model.** Nothing here says a particular model cheated. It says
143
+ which problems would not have caught it.
144
+ - **It runs the benchmark's code.** Reference solutions, mutants and tests are executed, each job in
145
+ its own throwaway directory (some benchmark tasks write files; v0.1 let them land in your working
146
+ folder). That is isolation from your files, not a security sandbox: only run it on benchmarks
147
+ you'd be willing to execute.
148
+
149
+ ## Reproducing the headline numbers
150
+
151
+ ```
152
+ gameability check mbpp --json mbpp.json
153
+ gameability check humaneval --json humaneval.json
154
+ ```
155
+
156
+ Defaults: 24 mutants per problem, 8 fresh inputs, seed 0, contract inputs. Add `--inputs free` to
157
+ reproduce v0.1's numbers (MBPP 18.5%, HumanEval 11.8%). The known-answer tests run with
158
+ `python tests/test_gameability.py` and `python tests/test_contract_inputs.py`.
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,143 @@
1
+ # gameability
2
+
3
+ **Find the problems in a code benchmark that accept broken solutions.**
4
+
5
+ When a model learns from tests, the tests are its reward. A self-improving system trained on tests that
6
+ accept broken code learns to pass weak tests, not to write correct code. `gameability` measures how often
7
+ that can happen. It is part of the verification layer we use at Empyrean Intelligence for self-improvement
8
+ work, released for anyone who builds benchmarks or RL training sets.
9
+
10
+ A benchmark score means something only if the benchmark rejects solutions that lack the
11
+ skill it measures. `gameability` checks that directly. It takes each problem's reference
12
+ solution, breaks it in one small place, confirms the result really is wrong, and then
13
+ asks whether the problem's own tests notice.
14
+
15
+ ```
16
+ pip install "gameability[hf]"
17
+ gameability check mbpp
18
+ ```
19
+
20
+ On MBPP, **17.9% of problems (169 of 942 judged) accept a broken solution**; on HumanEval,
21
+ **10.3% (16 of 155)**. Each run takes minutes on a laptop, with no model and no API key.
22
+
23
+ **Confirmed on valid inputs.** On the problems EvalPlus also covers, every accepted broken solution
24
+ was re-run against EvalPlus's independent tests, whose inputs respect each problem's rules:
25
+ **95.2% of flagged MBPP problems and 84.6% of flagged HumanEval problems are confirmed** (the broken
26
+ solution passes the original tests and fails EvalPlus). The two unconfirmed HumanEval problems
27
+ (116 and 129) are ones whose own visible tests contradict their written specification.
28
+
29
+ **What changed in v0.2.** v0.1 built fresh inputs by changing values freely, which sometimes asked
30
+ a problem about inputs it never promised to handle (an unsorted list for binary search, random
31
+ letters for a parenthesis parser). v0.2 learns each argument's domain from the problem's own
32
+ visible tests and stays inside it: number signs and minimums, the characters or whole tokens a
33
+ string uses, and whether lists are sorted, unique, rectangular or a permutation. On the same
34
+ problems this raised the confirmed share from 85.7% to 95.2% (MBPP) and 78.6% to 84.6% (HumanEval)
35
+ while keeping 93.1% of v0.1's confirmed detections. `--inputs free` reproduces v0.1.
36
+
37
+ ## Scoreboard
38
+
39
+ Problems that accept at least one broken solution (wrong on at least one fresh input), with 95% intervals. Defaults (24 broken
40
+ versions per problem, 8 fresh inputs, seed 0), laptop CPU, October 2026.
41
+
42
+ | Benchmark | Weak problems | Command |
43
+ |---|---|---|
44
+ | MBPP | 17.9% (169 of 942) [15.6, 20.5] | `gameability check mbpp` |
45
+ | MBPP sanitized | 13.8% (57 of 413) [10.8, 17.5] | `gameability check mbpp-sanitized` |
46
+ | HumanEval | 10.3% (16 of 155) [6.5, 16.1] | `gameability check humaneval` |
47
+ | KodCode (RL training set, 1,000-row random sample) | 9.7% (45 of 466) [7.3, 12.7] | `gameability check kodcode:rows.jsonl` |
48
+ | HumanEval+ (EvalPlus) | 3.4% (5 of 147) [1.5, 7.7] | `gameability check humaneval-plus` |
49
+ | MBPP+ (EvalPlus) | 2.0% (7 of 353) [1.0, 4.0] | `gameability check mbpp-plus` |
50
+
51
+ EvalPlus's stronger tests look like a large improvement, but EvalPlus also rewrote most reference
52
+ solutions. Holding the original reference fixed (`mbpp-plus-origref`, `humaneval-plus-origref`), the weak
53
+ share roughly halves (MBPP 8.7% to 3.1% on 254 problems, HumanEval 10.6% to 4.9% on 123), and at these
54
+ sample sizes the intervals overlap. KodCode's judged set is the problems whose tests call the function with
55
+ literal arguments (466 of 801 loadable). `rows.jsonl` is KodCode rows saved as JSON lines; the sample above
56
+ is reproducible with `python tests/sample_kodcode.py rows.jsonl` (positions from `random.Random(0)`).
57
+
58
+ ## What it prints
59
+
60
+ ```
61
+ 415 -- 6 of 11 broken solutions pass every visible test
62
+ - if arr_len < 2:
63
+ + if arr_len <= 2:
64
+ a test that catches it: assert max_Product([4, 9]) == (4, 9)
65
+ ```
66
+
67
+ For every weak problem you get the one line that was changed, how many broken versions got
68
+ through, and an assert that would have caught one.
69
+
70
+ **`--fixes fixes.py` writes those asserts to a file; review them before adding them.** Measured on
71
+ 34 MBPP/HumanEval problems against a 1.5B model's real solutions: v0.2's suggested asserts wrongly
72
+ reject **2.6% of its correct solutions** (8 of 307; v0.1: 15.3%). They target the broken versions
73
+ gameability builds, not the mistakes a particular model makes: they caught 4 of that model's 24
74
+ real wrong solutions. Treat them as leads, not as a test suite.
75
+
76
+ ## How it works
77
+
78
+ For each problem the benchmark supplies (reference solution, visible tests):
79
+
80
+ 1. The reference must pass its own visible tests. If it doesn't, the problem is excluded and counted.
81
+ 2. Fresh inputs are built by varying the arguments in the visible tests. The reference's
82
+ outputs on them are the hidden answer key.
83
+ 3. The reference is mutated at a single site: a comparison flipped, an operator swapped, a
84
+ constant nudged, a `return` replaced with `return None`.
85
+ 4. A mutant counts as **broken** only if it gives a different answer from the reference on
86
+ at least one fresh input. Mutants that behave identically are thrown away, so they can't
87
+ inflate the number.
88
+ 5. A broken mutant that still passes every visible test is a **survivor**. A problem with at
89
+ least one survivor accepts a wrong solution.
90
+
91
+ Benchmark code runs in a child process with a step budget, so a runaway mutant is stopped at
92
+ the same point on any machine; a wall-clock limit is only a backstop. Mutants that never
93
+ finish are reported as truncated, never counted as rejected.
94
+
95
+ ## Your own benchmark
96
+
97
+ One problem per line in a `.jsonl` file:
98
+
99
+ ```json
100
+ {"id": "clamp", "reference": "def clamp(x, lo, hi): ...", "tests": ["assert clamp(5, 0, 10) == 5"]}
101
+ ```
102
+
103
+ Optional fields: `entry` (the function name, if it can't be inferred), `setup` (code run
104
+ before the tests), `hidden` (held-out asserts you already have; they are used as extra
105
+ answer-key inputs), `call_alias` (the name the tests call the solution by, e.g. `candidate`).
106
+
107
+ ```
108
+ gameability check examples/tiny.jsonl --fixes fixes.py --json report.json
109
+ ```
110
+
111
+ ## What the number means, and what it doesn't
112
+
113
+ - **The attacker is weak.** One small edit is the simplest possible way to be wrong; a model trained
114
+ against these tests can search far more. In that sense the rates are a floor.
115
+ - **Fresh inputs can still break a problem's rules in ways the tests don't reveal.** v0.2 keeps each
116
+ argument inside its observed domain, but it does not model relationships BETWEEN arguments (a
117
+ length argument that must equal a list's length) or rules that only the docstring states. On the
118
+ problems EvalPlus covers, 4.8% of flagged MBPP and 15.4% of flagged HumanEval problems are still
119
+ unconfirmed (v0.1: 14.3% and 21.4%).
120
+ - **It does not predict where a model will cheat.** On a 1.5B coder, false passes were no more
121
+ common on flagged problems than on others (8.4% vs 10.0%). It audits the tests' strength, not a
122
+ model's behaviour.
123
+ - **It audits the tests, not any model.** Nothing here says a particular model cheated. It says
124
+ which problems would not have caught it.
125
+ - **It runs the benchmark's code.** Reference solutions, mutants and tests are executed, each job in
126
+ its own throwaway directory (some benchmark tasks write files; v0.1 let them land in your working
127
+ folder). That is isolation from your files, not a security sandbox: only run it on benchmarks
128
+ you'd be willing to execute.
129
+
130
+ ## Reproducing the headline numbers
131
+
132
+ ```
133
+ gameability check mbpp --json mbpp.json
134
+ gameability check humaneval --json humaneval.json
135
+ ```
136
+
137
+ Defaults: 24 mutants per problem, 8 fresh inputs, seed 0, contract inputs. Add `--inputs free` to
138
+ reproduce v0.1's numbers (MBPP 18.5%, HumanEval 11.8%). The known-answer tests run with
139
+ `python tests/test_gameability.py` and `python tests/test_contract_inputs.py`.
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gameability"
7
+ version = "0.2.0"
8
+ description = "Find the problems in a code benchmark that accept broken solutions."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Empyrean Intelligence" }]
14
+ dependencies = []
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
18
+ "Topic :: Software Development :: Testing",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ hf = ["datasets"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/Empyrean-Intelligence/gameability"
26
+ Source = "https://github.com/Empyrean-Intelligence/gameability"
27
+ Issues = "https://github.com/Empyrean-Intelligence/gameability/issues"
28
+
29
+ [project.scripts]
30
+ gameability = "gameability.cli:main"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ """gameability -- find the problems in a code benchmark that accept broken solutions.
2
+
3
+ A benchmark is only as strong as its ability to REJECT a solution that provably
4
+ lacks the capability it claims to measure. This package manufactures such
5
+ solutions, proves they are genuinely broken against inputs the benchmark never
6
+ shows, and reports what fraction the benchmark accepts anyway.
7
+
8
+ from gameability import Problem, census, summarise, report
9
+
10
+ rows = census([Problem("p1", reference_src, ["assert f(1) == 2"])])
11
+ report(summarise(rows, "my benchmark"))
12
+
13
+ The only thing a benchmark must supply is (reference solution, visible tests).
14
+ """
15
+ from .engine import Problem, audit, census, report, summarise
16
+ from .harden import witnesses
17
+
18
+ __all__ = ["Problem", "audit", "census", "summarise", "report", "witnesses"]
19
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,87 @@
1
+ """Child process: execute one problem's reference and mutants in isolation.
2
+
3
+ Results are emitted as one JSON line per candidate and flushed immediately, so
4
+ a candidate that fails to terminate costs only the candidates after it rather
5
+ than the whole problem. The parent records the shortfall as `truncated` instead
6
+ of silently treating unreached mutants as killed, which would bias the headline
7
+ number downward -- toward the flattering answer.
8
+ """
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+ STEP_BUDGET = 200_000
14
+
15
+
16
+ class _Budget:
17
+ """Abort runaway code by counting traced events. Windows has no SIGALRM."""
18
+
19
+ def __init__(self, limit):
20
+ self.n, self.limit = 0, limit
21
+
22
+ def __call__(self, frame, event, arg):
23
+ self.n += 1
24
+ if self.n > self.limit:
25
+ raise TimeoutError("step budget exceeded")
26
+ return self
27
+
28
+
29
+ def outcome(ns, expr):
30
+ """('ok', repr) or ('exc', type) -- repr avoids trusting a custom __eq__."""
31
+ sys.settrace(_Budget(STEP_BUDGET))
32
+ try:
33
+ return ["ok", repr(eval(expr, ns))]
34
+ except BaseException as e:
35
+ return ["exc", type(e).__name__]
36
+ finally:
37
+ sys.settrace(None)
38
+
39
+
40
+ def run_asserts(ns, tests):
41
+ for t in tests:
42
+ sys.settrace(_Budget(STEP_BUDGET))
43
+ try:
44
+ exec(t, ns)
45
+ except BaseException:
46
+ return False
47
+ finally:
48
+ sys.settrace(None)
49
+ return True
50
+
51
+
52
+ def evaluate(src, setup, tests, fn, fresh):
53
+ ns = {"__name__": "__candidate__"}
54
+ try:
55
+ if setup:
56
+ exec(setup, ns)
57
+ exec(src, ns)
58
+ except BaseException as e:
59
+ return {"loaded": False, "error": type(e).__name__}
60
+ if fn not in ns:
61
+ return {"loaded": False, "error": "entry point %r not defined" % fn}
62
+ return {
63
+ "loaded": True,
64
+ "visible": run_asserts(dict(ns), tests),
65
+ "hidden": [outcome(dict(ns), "%s(%s)" % (fn, a)) for a in fresh],
66
+ }
67
+
68
+
69
+ def main():
70
+ job = json.loads(sys.stdin.read())
71
+ setup, tests = job.get("setup", ""), job["tests"]
72
+ fn, fresh = job["fn"], job["fresh"]
73
+ # Benchmark code prints. Its output must never reach the result stream, where a
74
+ # printed number or dict would be read as a record (2026-10-17: a KodCode test
75
+ # printed an int and crashed the reader). Results go to the real stdout only.
76
+ out = sys.stdout
77
+ sys.stdout = open(os.devnull, "w")
78
+ for tag, src in [("ref", job["ref"])] + [
79
+ ("m%d" % i, m) for i, m in enumerate(job["mutants"])]:
80
+ rec = evaluate(src, setup, tests, fn, fresh)
81
+ rec["tag"] = tag
82
+ out.write(json.dumps(rec) + "\n")
83
+ out.flush()
84
+
85
+
86
+ if __name__ == "__main__":
87
+ main()
@@ -0,0 +1,135 @@
1
+ """Command line: gameability check <benchmark>
2
+
3
+ gameability check mbpp
4
+ gameability check humaneval --limit 50
5
+ gameability check my_benchmark.jsonl --fixes fixes.py --json report.json
6
+ """
7
+ import argparse
8
+ import ast
9
+ import difflib
10
+ import json
11
+ import os
12
+ import random
13
+ import sys
14
+ import time
15
+ import warnings
16
+
17
+ # Benchmark references are third-party code; old-style escapes in them ('\w')
18
+ # make Python warn on every parse. The warnings say nothing about the audit.
19
+ warnings.simplefilter("ignore", SyntaxWarning)
20
+
21
+ from . import __version__
22
+ from .engine import census, report, summarise
23
+ from .loaders import SOURCES, from_jsonl, kodcode
24
+
25
+
26
+ def _changed_lines(reference, mutant):
27
+ """The one line the mutation changed, before and after (normalised by ast)."""
28
+ try:
29
+ a = ast.unparse(ast.parse(reference)).splitlines()
30
+ except SyntaxError:
31
+ a = reference.splitlines()
32
+ b = mutant.splitlines()
33
+ out = []
34
+ for line in difflib.unified_diff(a, b, lineterm="", n=0):
35
+ if line.startswith(("---", "+++", "@@")):
36
+ continue
37
+ out.append(line)
38
+ return out
39
+
40
+
41
+ def _load(target):
42
+ if target in SOURCES:
43
+ return SOURCES[target]()
44
+ if target.startswith("kodcode:"):
45
+ return kodcode(target[len("kodcode:"):])
46
+ if os.path.isfile(target):
47
+ return from_jsonl(target)
48
+ raise SystemExit("unknown benchmark %r: use %s, or a path to a .jsonl file" % (target, ", ".join(SOURCES)))
49
+
50
+
51
+ def check(a):
52
+ problems, dropped = _load(a.benchmark)
53
+ label = os.path.basename(a.benchmark)
54
+ print("%s: %d problems usable as (reference, visible tests)" % (label, len(problems)))
55
+ if dropped:
56
+ print(" %d dropped before the audit (no tests, or the tests never call a "
57
+ "function the reference defines)" % dropped)
58
+ if a.limit:
59
+ random.Random(a.seed).shuffle(problems)
60
+ problems = problems[:a.limit]
61
+ print(" random sample of %d, seed %d" % (len(problems), a.seed))
62
+ if not problems:
63
+ raise SystemExit("nothing to audit")
64
+
65
+ t0 = time.time()
66
+ rows = census(problems, workers=a.workers, seed=a.seed, n_mutants=a.mutants,
67
+ timeout=a.timeout, want_witness=True, inputs=a.inputs)
68
+ s = summarise(rows, "%s (%d problems)" % (label, len(problems)))
69
+ print("\nelapsed %.0fs\n" % (time.time() - t0))
70
+ report(s)
71
+
72
+ by_id = {p.pid: p for p in problems}
73
+ weak = [r for r in rows if r["status"] == "ok" and r["n_survived"] > 0]
74
+ if weak and a.show:
75
+ print("\nPROBLEMS THAT ACCEPT A BROKEN SOLUTION (first %d of %d)" % (min(a.show, len(weak)), len(weak)))
76
+ for r in weak[:a.show]:
77
+ ex = r["examples"][0]
78
+ print("\n %s -- %d of %d broken solutions pass every visible test" % (r["pid"], r["n_survived"], r["n_broken"]))
79
+ for line in _changed_lines(by_id[r["pid"]].reference, ex["src"]):
80
+ print(" " + line)
81
+ if r.get("witness"):
82
+ args, out = r["witness"][0]
83
+ print(" a test that catches it: assert %s(%s) == %s" % (by_id[r["pid"]].entry, args, out))
84
+
85
+ if a.fixes:
86
+ n = 0
87
+ with open(a.fixes, "w", encoding="utf-8") as f:
88
+ f.write("# Asserts that reject the broken solutions gameability found.\n")
89
+ f.write("# Expected values come from each problem's own reference solution;\n")
90
+ f.write("# review them before adding them to the benchmark.\n")
91
+ for r in weak:
92
+ seen = set()
93
+ for args, out in r.get("witness", []):
94
+ t = "assert %s(%s) == %s" % (by_id[r["pid"]].entry, args, out)
95
+ if t not in seen:
96
+ seen.add(t)
97
+ if len(seen) == 1:
98
+ f.write("\n# %s\n" % r["pid"])
99
+ f.write(t + "\n")
100
+ n += 1
101
+ print("\nwrote %d suggested asserts for %d problems to %s" % (n, len(weak), a.fixes))
102
+
103
+ if a.json:
104
+ with open(a.json, "w", encoding="utf-8") as f:
105
+ json.dump({"version": __version__, "summary": s, "rows": rows}, f, indent=1)
106
+ print("wrote %s" % a.json)
107
+ return 0
108
+
109
+
110
+ def main(argv=None):
111
+ ap = argparse.ArgumentParser(prog="gameability",
112
+ description="Find the problems in a code benchmark that accept broken solutions.")
113
+ ap.add_argument("--version", action="version", version="gameability " + __version__)
114
+ sub = ap.add_subparsers(dest="cmd", required=True)
115
+ c = sub.add_parser("check", help="audit a benchmark")
116
+ c.add_argument("benchmark", help="mbpp, humaneval, or a .jsonl file")
117
+ c.add_argument("--limit", type=int, default=0, help="audit a random sample of N problems")
118
+ c.add_argument("--seed", type=int, default=0)
119
+ c.add_argument("--workers", type=int, default=8)
120
+ c.add_argument("--mutants", type=int, default=24, help="broken solutions to try per problem")
121
+ c.add_argument("--inputs", choices=["contract", "free"], default="contract",
122
+ help="contract (default): fresh inputs stay inside each argument's observed domain; "
123
+ "free: v0.1's unconstrained generator, to reproduce its numbers")
124
+ c.add_argument("--timeout", type=int, default=40,
125
+ help="wall-clock backstop per problem in seconds (the step budget is the real limit)")
126
+ c.add_argument("--show", type=int, default=5, help="print this many weak problems with the changed line")
127
+ c.add_argument("--fixes", help="write suggested asserts to this file")
128
+ c.add_argument("--json", help="write the full report to this file")
129
+ a = ap.parse_args(argv)
130
+ if a.cmd == "check":
131
+ return check(a)
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())