gameability 0.2.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.
- gameability/__init__.py +19 -0
- gameability/__main__.py +5 -0
- gameability/_worker.py +87 -0
- gameability/cli.py +135 -0
- gameability/engine.py +275 -0
- gameability/harden.py +206 -0
- gameability/loaders.py +290 -0
- gameability/mutate.py +155 -0
- gameability/oracle.py +232 -0
- gameability-0.2.0.dist-info/METADATA +162 -0
- gameability-0.2.0.dist-info/RECORD +15 -0
- gameability-0.2.0.dist-info/WHEEL +5 -0
- gameability-0.2.0.dist-info/entry_points.txt +2 -0
- gameability-0.2.0.dist-info/licenses/LICENSE +21 -0
- gameability-0.2.0.dist-info/top_level.txt +1 -0
gameability/oracle.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Hidden inputs, derived from the benchmark's own visible tests.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS
|
|
4
|
+
A mutant that survives the visible tests proves nothing on its own -- it might
|
|
5
|
+
be an EQUIVALENT mutant, semantically identical to the reference, in which case
|
|
6
|
+
passing is correct and the test suite is blameless. Counting those as gameable
|
|
7
|
+
would be the single easiest way to manufacture an impressive false result.
|
|
8
|
+
|
|
9
|
+
So every surviving mutant must first be shown genuinely broken, and that needs
|
|
10
|
+
inputs the visible tests never show. We build them by perturbing the literal
|
|
11
|
+
arguments already present in the benchmark's asserts: the types stay right by
|
|
12
|
+
construction, so no type inference is needed, and the values are fresh.
|
|
13
|
+
|
|
14
|
+
This is the same construction that took a lookup table over our own generated
|
|
15
|
+
problems from 60/60 to 0/60.
|
|
16
|
+
"""
|
|
17
|
+
import ast
|
|
18
|
+
import random
|
|
19
|
+
import string
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_calls(tests, fn=None):
|
|
23
|
+
"""Pull `fn(literal, ...)` argument tuples out of a list of assert strings."""
|
|
24
|
+
calls, name = [], fn
|
|
25
|
+
for t in tests:
|
|
26
|
+
try:
|
|
27
|
+
tree = ast.parse(t.strip())
|
|
28
|
+
except SyntaxError:
|
|
29
|
+
continue
|
|
30
|
+
for n in ast.walk(tree):
|
|
31
|
+
if not isinstance(n, ast.Call) or not isinstance(n.func, ast.Name):
|
|
32
|
+
continue
|
|
33
|
+
if name is None:
|
|
34
|
+
name = n.func.id
|
|
35
|
+
if n.func.id != name or n.keywords:
|
|
36
|
+
continue
|
|
37
|
+
try:
|
|
38
|
+
args = [ast.literal_eval(a) for a in n.args]
|
|
39
|
+
except (ValueError, SyntaxError, TypeError):
|
|
40
|
+
continue
|
|
41
|
+
calls.append(args)
|
|
42
|
+
return name, calls
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def entry_name(tests):
|
|
46
|
+
"""The function the asserts call most often -- the problem's entry point."""
|
|
47
|
+
counts = {}
|
|
48
|
+
for t in tests:
|
|
49
|
+
try:
|
|
50
|
+
tree = ast.parse(t.strip())
|
|
51
|
+
except SyntaxError:
|
|
52
|
+
continue
|
|
53
|
+
for n in ast.walk(tree):
|
|
54
|
+
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name):
|
|
55
|
+
counts[n.func.id] = counts.get(n.func.id, 0) + 1
|
|
56
|
+
if not counts:
|
|
57
|
+
return None
|
|
58
|
+
return max(sorted(counts), key=lambda k: counts[k])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def perturb(v, rng, depth=0):
|
|
62
|
+
"""Return a fresh value of the same shape and type as `v`."""
|
|
63
|
+
if depth > 4:
|
|
64
|
+
return v
|
|
65
|
+
if isinstance(v, bool):
|
|
66
|
+
return not v
|
|
67
|
+
if isinstance(v, int):
|
|
68
|
+
d = rng.choice([-3, -2, -1, 1, 2, 3, 5, 7])
|
|
69
|
+
return v + d
|
|
70
|
+
if isinstance(v, float):
|
|
71
|
+
return round(v + rng.uniform(-3, 3), 4)
|
|
72
|
+
if isinstance(v, str):
|
|
73
|
+
if not v:
|
|
74
|
+
return rng.choice(["a", "ab", "xyz"])
|
|
75
|
+
pool = string.ascii_lowercase
|
|
76
|
+
if v.isupper():
|
|
77
|
+
pool = string.ascii_uppercase
|
|
78
|
+
elif v.isdigit():
|
|
79
|
+
pool = string.digits
|
|
80
|
+
n = max(1, len(v) + rng.choice([-1, 0, 0, 1]))
|
|
81
|
+
return "".join(rng.choice(pool) for _ in range(n))
|
|
82
|
+
if isinstance(v, (list, tuple, set)):
|
|
83
|
+
items = list(v)
|
|
84
|
+
if not items:
|
|
85
|
+
return type(v)()
|
|
86
|
+
n = max(1, len(items) + rng.choice([-1, 0, 0, 1]))
|
|
87
|
+
base = [items[i % len(items)] for i in range(n)]
|
|
88
|
+
out = [perturb(x, rng, depth + 1) for x in base]
|
|
89
|
+
return type(v)(out) if not isinstance(v, set) else set(out)
|
|
90
|
+
if isinstance(v, dict):
|
|
91
|
+
return {k: perturb(val, rng, depth + 1) for k, val in v.items()}
|
|
92
|
+
return v
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---- CONTRACT-PRESERVING INPUTS (v0.2, contract_inputs_v1)
|
|
96
|
+
# The free generator above changes values with no knowledge of a problem's rules, so it can ask a
|
|
97
|
+
# binary search about an unsorted list or a parenthesis parser about random letters. On such inputs the
|
|
98
|
+
# reference's answer is arbitrary, and a mutant that differs there was counted as broken: measured on the
|
|
99
|
+
# problems EvalPlus covers, 14% of flagged MBPP and 21% of flagged HumanEval problems were flagged only by
|
|
100
|
+
# such inputs (census_contract_check_v1). This generator learns each argument's observed domain from ALL
|
|
101
|
+
# of a problem's visible calls and stays inside it.
|
|
102
|
+
|
|
103
|
+
def _is_sorted(x):
|
|
104
|
+
try:
|
|
105
|
+
return all(x[i] <= x[i + 1] for i in range(len(x) - 1))
|
|
106
|
+
except TypeError:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def profile(values):
|
|
111
|
+
"""What every observed value at one argument position has in common."""
|
|
112
|
+
vals = [v for v in values]
|
|
113
|
+
if not vals:
|
|
114
|
+
return {"kind": "any"}
|
|
115
|
+
if all(isinstance(v, bool) for v in vals):
|
|
116
|
+
return {"kind": "bool"}
|
|
117
|
+
if all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in vals):
|
|
118
|
+
lo = min(vals)
|
|
119
|
+
return {"kind": "num", "lb": 1 if lo >= 1 else (0 if lo >= 0 else None)}
|
|
120
|
+
if all(isinstance(v, str) for v in vals):
|
|
121
|
+
spaced = any(" " in v.strip() for v in vals)
|
|
122
|
+
tokens = sorted({t for v in vals for t in v.split()}) if spaced else []
|
|
123
|
+
chars = sorted({c for v in vals for c in v})
|
|
124
|
+
return {"kind": "str", "chars": chars, "tokens": tokens}
|
|
125
|
+
if all(isinstance(v, (list, tuple)) for v in vals):
|
|
126
|
+
evid = [v for v in vals if len(v) >= 2]
|
|
127
|
+
flat = [x for v in vals for x in v]
|
|
128
|
+
rect = bool(vals) and all(len(v) >= 1 and all(isinstance(r, (list, tuple)) for r in v) and
|
|
129
|
+
len({len(r) for r in v}) == 1 for v in vals)
|
|
130
|
+
square = rect and all(len(v) == len(v[0]) for v in vals)
|
|
131
|
+
def perm1n(v):
|
|
132
|
+
f = [x for r in v for x in r] if rect else list(v)
|
|
133
|
+
return all(isinstance(x, int) and not isinstance(x, bool) for x in f) and sorted(f) == list(range(1, len(f) + 1))
|
|
134
|
+
return {"kind": "seq",
|
|
135
|
+
"sorted": bool(evid) and not rect and all(_is_sorted(v) for v in evid),
|
|
136
|
+
"unique": bool(evid) and not rect and all(len(set(map(repr, v))) == len(v) for v in evid),
|
|
137
|
+
"rect": rect, "square": square,
|
|
138
|
+
"perm1n": bool(evid) and all(perm1n(v) for v in vals),
|
|
139
|
+
"elem": profile([x for r in flat for x in r] if rect else flat)}
|
|
140
|
+
return {"kind": "any"}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def perturb_contract(v, rng, prof, depth=0):
|
|
144
|
+
"""A fresh value of the same shape and type as `v`, inside the domain `prof` was learned from."""
|
|
145
|
+
k = prof.get("kind")
|
|
146
|
+
if depth > 4 or k == "any":
|
|
147
|
+
return perturb(v, rng, depth)
|
|
148
|
+
if k == "bool" and isinstance(v, bool):
|
|
149
|
+
return not v
|
|
150
|
+
if k == "num" and isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
151
|
+
out = v + rng.choice([-3, -2, -1, 1, 2, 3, 5, 7]) if isinstance(v, int) else round(v + rng.uniform(-3, 3), 4)
|
|
152
|
+
lb = prof.get("lb")
|
|
153
|
+
if lb is not None and out < lb:
|
|
154
|
+
out = lb + (lb - out) # reflect back inside the observed domain
|
|
155
|
+
return out
|
|
156
|
+
if k == "str" and isinstance(v, str):
|
|
157
|
+
if prof["tokens"]: # whitespace-separated tokens: recombine observed tokens
|
|
158
|
+
n = max(1, len(v.split()) + rng.choice([-1, 0, 0, 1]))
|
|
159
|
+
return " ".join(rng.choice(prof["tokens"]) for _ in range(n))
|
|
160
|
+
pool = prof["chars"] or list(string.ascii_lowercase)
|
|
161
|
+
n = max(1, len(v) + rng.choice([-1, 0, 0, 1])) if v else 1
|
|
162
|
+
return "".join(rng.choice(pool) for _ in range(n))
|
|
163
|
+
if k == "seq" and isinstance(v, (list, tuple)):
|
|
164
|
+
if not v:
|
|
165
|
+
return type(v)()
|
|
166
|
+
if prof["rect"]:
|
|
167
|
+
rows, cols = len(v), len(v[0])
|
|
168
|
+
if prof["square"]:
|
|
169
|
+
rows = cols = max(1, rows + rng.choice([-1, 0, 0, 1]))
|
|
170
|
+
else:
|
|
171
|
+
rows = max(1, rows + rng.choice([-1, 0, 0, 1]))
|
|
172
|
+
if prof["perm1n"]:
|
|
173
|
+
vals = list(range(1, rows * cols + 1)); rng.shuffle(vals)
|
|
174
|
+
else:
|
|
175
|
+
src = [x for r in v for x in r]
|
|
176
|
+
vals = [perturb_contract(src[i % len(src)], rng, prof["elem"], depth + 1) for i in range(rows * cols)]
|
|
177
|
+
out = [vals[i * cols:(i + 1) * cols] for i in range(rows)]
|
|
178
|
+
return type(v)(type(v[0])(r) for r in out)
|
|
179
|
+
n = max(1, len(v) + rng.choice([-1, 0, 0, 1]))
|
|
180
|
+
if prof["perm1n"]:
|
|
181
|
+
out = list(range(1, n + 1)); rng.shuffle(out)
|
|
182
|
+
else:
|
|
183
|
+
base = [v[i % len(v)] for i in range(n)]
|
|
184
|
+
out = [perturb_contract(x, rng, prof["elem"], depth + 1) for x in base]
|
|
185
|
+
if prof["unique"]:
|
|
186
|
+
seen, uniq = set(), []
|
|
187
|
+
for x in out:
|
|
188
|
+
tries = 0
|
|
189
|
+
while repr(x) in seen and tries < 20:
|
|
190
|
+
x = perturb_contract(x, rng, prof["elem"], depth + 1); tries += 1
|
|
191
|
+
if repr(x) not in seen:
|
|
192
|
+
seen.add(repr(x)); uniq.append(x)
|
|
193
|
+
out = uniq or out[:1]
|
|
194
|
+
if prof["sorted"]:
|
|
195
|
+
try:
|
|
196
|
+
out = sorted(out)
|
|
197
|
+
except TypeError:
|
|
198
|
+
pass
|
|
199
|
+
return type(v)(out)
|
|
200
|
+
return perturb(v, rng, depth)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def fresh_calls(calls, n=8, seed=0, inputs="contract"):
|
|
204
|
+
"""Fresh argument tuples, as source text, ready to be executed.
|
|
205
|
+
|
|
206
|
+
inputs="contract" (default from v0.2) stays inside each argument's observed domain;
|
|
207
|
+
inputs="free" is v0.1's generator, kept so its published numbers remain reproducible."""
|
|
208
|
+
if not calls:
|
|
209
|
+
return []
|
|
210
|
+
if inputs not in ("contract", "free"):
|
|
211
|
+
raise ValueError("inputs must be 'contract' or 'free', got %r" % (inputs,))
|
|
212
|
+
rng = random.Random(seed)
|
|
213
|
+
profs = {}
|
|
214
|
+
if inputs == "contract":
|
|
215
|
+
for ar in {len(c) for c in calls}:
|
|
216
|
+
same = [c for c in calls if len(c) == ar]
|
|
217
|
+
profs[ar] = [profile([c[j] for c in same]) for j in range(ar)]
|
|
218
|
+
out, seen = [], {repr(c) for c in calls}
|
|
219
|
+
for i in range(n * 4):
|
|
220
|
+
if len(out) >= n:
|
|
221
|
+
break
|
|
222
|
+
base = calls[i % len(calls)]
|
|
223
|
+
if inputs == "contract":
|
|
224
|
+
cand = [perturb_contract(a, rng, p) for a, p in zip(base, profs[len(base)])]
|
|
225
|
+
else:
|
|
226
|
+
cand = [perturb(a, rng) for a in base]
|
|
227
|
+
r = repr(cand)
|
|
228
|
+
if r in seen:
|
|
229
|
+
continue
|
|
230
|
+
seen.add(r)
|
|
231
|
+
out.append(", ".join(repr(a) for a in cand))
|
|
232
|
+
return out
|
|
@@ -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,15 @@
|
|
|
1
|
+
gameability/__init__.py,sha256=rIBy1Ta8EceZbcm1O-I7qdZe1MeInSAalhlIPZ1l66c,840
|
|
2
|
+
gameability/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
|
|
3
|
+
gameability/_worker.py,sha256=JS1d-Ues0E8BlInXLRcgZKpIfizK_-kMazjUzwFwW3I,2666
|
|
4
|
+
gameability/cli.py,sha256=LqHBdalFFVOeYmD4zTGh12cMAIu-sthosoK_MH3WKJs,5787
|
|
5
|
+
gameability/engine.py,sha256=qYxEriBrUZBnX7Rxsbjj-kNUbQspVjUWVow6qufbVI4,12665
|
|
6
|
+
gameability/harden.py,sha256=5pjtmXZIUfw2ilCNB2s2D-C6LtuIBt7Mh2My2K4Bsoc,8773
|
|
7
|
+
gameability/loaders.py,sha256=LmauU6pYBEN82QkcEywyk_JQ8gB6M0Oqx-xBbDwomHs,12540
|
|
8
|
+
gameability/mutate.py,sha256=nU7prNSuVW_3DOL_WURQElNzwdEQbOTall_-Lv-7jDM,5842
|
|
9
|
+
gameability/oracle.py,sha256=UEgBS-jP0Uf4bjGpqaSzDH2qtRgjA6zrDC7RMX5jltQ,9965
|
|
10
|
+
gameability-0.2.0.dist-info/licenses/LICENSE,sha256=JSp4X4Yy5hzOBeIJRmEHwSi6QEJm17VINS1vdQRvq_s,1078
|
|
11
|
+
gameability-0.2.0.dist-info/METADATA,sha256=MKWcFo7EShcMOPzB_TJqrzfTRWPoGAeRIKStpP0nFZU,8692
|
|
12
|
+
gameability-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
13
|
+
gameability-0.2.0.dist-info/entry_points.txt,sha256=6TNGGnuquMJ-OBgCkbaLcQnP5K5MrcLh52X4rmS4Smc,53
|
|
14
|
+
gameability-0.2.0.dist-info/top_level.txt,sha256=L7ZC-VXXQ-SeaSz6gYLDHN9ZnudBeRRMs_6H1U0T3PU,12
|
|
15
|
+
gameability-0.2.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
gameability
|