dsh-eval-guard 0.7.2 → 0.7.4
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.
- package/ISSUES.md +239 -0
- package/README.md +48 -18
- package/assets/neural-cifar/workspace.zip +0 -0
- package/assets/neural-cpp/workspace.zip +0 -0
- package/assets/neural-fp32/workspace.zip +0 -0
- package/assets/neural-pendulum/SPEC.md +43 -35
- package/assets/neural-pendulum/TASK.md +139 -33
- package/assets/neural-pendulum/rl-feasibility/README.md +60 -0
- package/assets/neural-pendulum/rl-feasibility/RESULTS.md +370 -0
- package/assets/neural-pendulum/rl-feasibility/evaluate.py +107 -0
- package/assets/neural-pendulum/rl-feasibility/finalize_run.py +168 -0
- package/assets/neural-pendulum/rl-feasibility/models.py +124 -0
- package/assets/neural-pendulum/rl-feasibility/obs.py +205 -0
- package/assets/neural-pendulum/rl-feasibility/plot_selected.py +55 -0
- package/assets/neural-pendulum/rl-feasibility/reference_controller.py +112 -0
- package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
- package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +417 -0
- package/assets/neural-pendulum/rl-feasibility/results/summary.json +1599 -0
- package/assets/neural-pendulum/rl-feasibility/results/table.md +39 -0
- package/assets/neural-pendulum/rl-feasibility/sim.py +324 -0
- package/assets/neural-pendulum/rl-feasibility/summarize.py +175 -0
- package/assets/neural-pendulum/rl-feasibility/task.py +342 -0
- package/assets/neural-pendulum/rl-feasibility/test_contract.py +188 -0
- package/assets/neural-pendulum/rl-feasibility/train_ppo.py +403 -0
- package/assets/neural-pendulum/rl-feasibility/train_sac.py +406 -0
- package/assets/neural-pendulum/sim.cpp +13 -2
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/README.md +19 -9
- package/eval/bomberman.py +5 -3
- package/eval/commit0.py +10 -3
- package/eval/common.py +169 -5
- package/eval/ctf.py +4 -2
- package/eval/featurebench.py +12 -8
- package/eval/land.py +4 -2
- package/eval/legacy/bomberman.py +5 -2
- package/eval/legacy/ctf.py +5 -2
- package/eval/legacy/featurebench.py +55 -15
- package/eval/legacy/land.py +6 -3
- package/eval/legacy/swe_bench_pro.py +57 -16
- package/eval/legacy/tank.py +2 -2
- package/eval/legacy/warehouse.py +4 -4
- package/eval/neural-cpp.py +9 -6
- package/eval/neural-fp32.py +17 -5
- package/eval/swe-bench-pro.py +11 -7
- package/eval/tank.py +11 -2
- package/eval/warehouse.py +17 -1
- package/lib/client/index.js +56 -29
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +883 -298
- package/native/dsh-eval-supervisor +0 -0
- package/native/supervisor.c +232 -36
- package/neural-cifar-eval/README.md +5 -0
- package/neural-cifar-eval/common.py +13 -0
- package/neural-cifar-eval/ops_eval.py +17 -5
- package/neural-cifar-eval/run_eval.py +42 -32
- package/neural-pendulum-eval/README.md +62 -0
- package/neural-pendulum-eval/ops_eval.py +603 -0
- package/neural-pendulum-eval/run_eval.py +218 -137
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +7 -11
- package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
- package/assets/neural-pendulum/include/nn/nn.hpp +0 -88
package/eval/common.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
+
import hashlib
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import shutil
|
|
@@ -12,6 +13,22 @@ from pathlib import Path
|
|
|
12
13
|
|
|
13
14
|
EVAL_ROOT = Path(__file__).resolve().parent
|
|
14
15
|
|
|
16
|
+
# Variables the host injects for the evaluator itself. They must not leak into
|
|
17
|
+
# the legacy evaluator or into any candidate process it starts (a candidate
|
|
18
|
+
# that learns EVAL_RESULT_FILE could overwrite the result).
|
|
19
|
+
HOST_EVAL_KEYS = frozenset({
|
|
20
|
+
'EVAL_RESULT_FILE', 'EVAL_JOB_DIR', 'EVAL_INSTALL_TOKEN',
|
|
21
|
+
'EVAL_BASELINE_DIR', 'EVAL_VENV_MANIFEST',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def child_env() -> dict[str, str]:
|
|
26
|
+
"""Environment for trusted legacy evaluators and their candidate children."""
|
|
27
|
+
return {
|
|
28
|
+
key: value for key, value in os.environ.items()
|
|
29
|
+
if key not in HOST_EVAL_KEYS and key not in {'PYTHONPATH', 'PYTHONHOME'}
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
|
|
16
33
|
def parse_workspace() -> Path:
|
|
17
34
|
parser = argparse.ArgumentParser(description='single-workspace evaluator')
|
|
@@ -28,18 +45,23 @@ def emit(payload: dict) -> None:
|
|
|
28
45
|
Path(target).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
29
46
|
|
|
30
47
|
|
|
31
|
-
def clean_copy(source: Path, target: Path, ignore_venv: bool = False
|
|
48
|
+
def clean_copy(source: Path, target: Path, ignore_venv: bool = False,
|
|
49
|
+
ignore_names: tuple[str, ...] = ()) -> None:
|
|
50
|
+
ignored = set(ignore_names)
|
|
51
|
+
|
|
32
52
|
def ignore(directory: str, names: list[str]) -> set[str]:
|
|
33
|
-
if
|
|
34
|
-
|
|
53
|
+
if Path(directory) == source:
|
|
54
|
+
skip = set(ignored)
|
|
55
|
+
if ignore_venv:
|
|
56
|
+
skip.add('.venv')
|
|
57
|
+
return {name for name in names if name in skip}
|
|
35
58
|
return set()
|
|
36
59
|
shutil.copytree(source, target, symlinks=True, ignore=ignore)
|
|
37
60
|
|
|
38
61
|
|
|
39
62
|
def run_legacy(script: Path, args: list[str], out: Path) -> dict:
|
|
40
63
|
cmd = [sys.executable, '-X', 'utf8', str(script), *args, '--out', str(out)]
|
|
41
|
-
|
|
42
|
-
completed = subprocess.run(cmd, cwd=str(EVAL_ROOT), capture_output=True, text=True, encoding='utf-8', errors='replace', env=env, timeout=1800)
|
|
64
|
+
completed = subprocess.run(cmd, cwd=str(EVAL_ROOT), capture_output=True, text=True, encoding='utf-8', errors='replace', env=child_env(), timeout=1800)
|
|
43
65
|
if completed.returncode != 0:
|
|
44
66
|
return {'ok': False, 'error': (completed.stderr or completed.stdout or 'evaluator failed')[-4000:]}
|
|
45
67
|
summary = out / 'summary.json'
|
|
@@ -59,3 +81,145 @@ def candidate_row(summary: dict, name: str = 'candidate') -> dict:
|
|
|
59
81
|
def fail(payload: dict) -> int:
|
|
60
82
|
emit(payload)
|
|
61
83
|
return 1
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_pytest_results(output: str) -> dict[str, str]:
|
|
87
|
+
"""Map pytest node ids to PASSED/FAILED/ERROR/SKIPPED from -rA output."""
|
|
88
|
+
import re
|
|
89
|
+
pattern = re.compile(r'^(PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS) (\S+)', re.MULTILINE)
|
|
90
|
+
results: dict[str, str] = {}
|
|
91
|
+
for match in pattern.finditer(output):
|
|
92
|
+
results[match.group(2)] = match.group(1)
|
|
93
|
+
return results
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def matches_node(node: str, selector: str) -> bool:
|
|
97
|
+
"""Whether a pytest node id belongs to a file/node selector."""
|
|
98
|
+
if node == selector:
|
|
99
|
+
return True
|
|
100
|
+
return node.startswith(selector + '::') or node.startswith(selector + '/')
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Pristine test material and venv integrity
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def sha256_file(path: Path) -> str:
|
|
109
|
+
digest = hashlib.sha256()
|
|
110
|
+
digest.update(path.read_bytes())
|
|
111
|
+
return digest.hexdigest()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sha256_tree(root: Path) -> str:
|
|
115
|
+
digest = hashlib.sha256()
|
|
116
|
+
|
|
117
|
+
def walk(current: Path) -> None:
|
|
118
|
+
for entry in sorted(current.iterdir(), key=lambda item: item.name):
|
|
119
|
+
digest.update(str(entry.relative_to(root)).encode('utf-8'))
|
|
120
|
+
if entry.is_dir():
|
|
121
|
+
walk(entry)
|
|
122
|
+
elif entry.is_file():
|
|
123
|
+
digest.update(entry.read_bytes())
|
|
124
|
+
elif entry.is_symlink():
|
|
125
|
+
digest.update(b'link:' + os.readlink(entry).encode('utf-8'))
|
|
126
|
+
|
|
127
|
+
walk(root)
|
|
128
|
+
return digest.hexdigest()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def baseline_dir() -> Path | None:
|
|
132
|
+
raw = os.environ.get('EVAL_BASELINE_DIR', '')
|
|
133
|
+
if raw == '':
|
|
134
|
+
return None
|
|
135
|
+
path = Path(raw)
|
|
136
|
+
return path if path.is_dir() else None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def restore_baseline(candidate: Path) -> None:
|
|
140
|
+
"""Overwrite candidate test material with the pristine install-time copy.
|
|
141
|
+
|
|
142
|
+
Also removes conftest.py files the agent added (the pristine snapshot keeps
|
|
143
|
+
the project's own ones), so a workspace hook cannot rewrite pytest results.
|
|
144
|
+
"""
|
|
145
|
+
baseline = baseline_dir()
|
|
146
|
+
if baseline is None:
|
|
147
|
+
return
|
|
148
|
+
candidate = candidate.resolve()
|
|
149
|
+
baseline = baseline.resolve()
|
|
150
|
+
for hook in candidate.rglob('conftest.py'):
|
|
151
|
+
if hook.is_symlink():
|
|
152
|
+
hook.unlink()
|
|
153
|
+
continue
|
|
154
|
+
try:
|
|
155
|
+
relative = hook.relative_to(candidate)
|
|
156
|
+
except ValueError:
|
|
157
|
+
continue
|
|
158
|
+
if not (baseline / relative).is_file():
|
|
159
|
+
hook.unlink()
|
|
160
|
+
for entry in baseline.iterdir():
|
|
161
|
+
if entry.name in {'manifest.json', 'venv.json'}:
|
|
162
|
+
continue
|
|
163
|
+
destination = candidate / entry.name
|
|
164
|
+
if destination.exists() or destination.is_symlink():
|
|
165
|
+
if destination.is_dir() and not destination.is_symlink():
|
|
166
|
+
shutil.rmtree(destination, ignore_errors=True)
|
|
167
|
+
else:
|
|
168
|
+
destination.unlink()
|
|
169
|
+
if entry.is_dir():
|
|
170
|
+
shutil.copytree(entry, destination, symlinks=True)
|
|
171
|
+
else:
|
|
172
|
+
shutil.copy2(entry, destination)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def verify_venv(workspace: Path) -> str | None:
|
|
176
|
+
"""Verify the venv control plane against the install-time fingerprint.
|
|
177
|
+
|
|
178
|
+
Returns a human-readable reason when the venv was tampered with, else None.
|
|
179
|
+
"""
|
|
180
|
+
baseline = baseline_dir()
|
|
181
|
+
if baseline is None:
|
|
182
|
+
return None
|
|
183
|
+
manifest_path = baseline / 'venv.json'
|
|
184
|
+
if not manifest_path.is_file():
|
|
185
|
+
return None
|
|
186
|
+
try:
|
|
187
|
+
manifest = json.loads(manifest_path.read_text(encoding='utf-8'))
|
|
188
|
+
except Exception:
|
|
189
|
+
return 'venv manifest is corrupt'
|
|
190
|
+
venv = workspace / '.venv'
|
|
191
|
+
if not venv.is_dir():
|
|
192
|
+
return 'the evaluator requires the installed .venv; it is missing'
|
|
193
|
+
expected_python = manifest.get('python')
|
|
194
|
+
if isinstance(expected_python, str) and expected_python != '':
|
|
195
|
+
python = venv / 'bin' / 'python'
|
|
196
|
+
try:
|
|
197
|
+
actual = str(python.resolve())
|
|
198
|
+
except OSError:
|
|
199
|
+
actual = ''
|
|
200
|
+
if actual != expected_python:
|
|
201
|
+
return 'the .venv python interpreter was replaced'
|
|
202
|
+
for relative, expected in (manifest.get('files') or {}).items():
|
|
203
|
+
target = venv / relative
|
|
204
|
+
if not target.is_file():
|
|
205
|
+
return f'the .venv file {relative} is missing'
|
|
206
|
+
try:
|
|
207
|
+
actual = sha256_file(target)
|
|
208
|
+
except OSError as error:
|
|
209
|
+
return f'cannot read the .venv file {relative}: {error}'
|
|
210
|
+
if actual != expected:
|
|
211
|
+
return f'the .venv file {relative} was modified'
|
|
212
|
+
expected_pytest = manifest.get('pytest')
|
|
213
|
+
if isinstance(expected_pytest, str) and expected_pytest != '':
|
|
214
|
+
found = False
|
|
215
|
+
for site in venv.glob('lib*/python*/site-packages/pytest'):
|
|
216
|
+
found = True
|
|
217
|
+
try:
|
|
218
|
+
actual = sha256_tree(site)
|
|
219
|
+
except OSError as error:
|
|
220
|
+
return f'cannot read the venv pytest package: {error}'
|
|
221
|
+
if actual != expected_pytest:
|
|
222
|
+
return 'the venv pytest package was modified'
|
|
223
|
+
if not found:
|
|
224
|
+
return 'the venv pytest package disappeared'
|
|
225
|
+
return None
|
package/eval/ctf.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Single-workspace
|
|
2
|
+
"""Single-workspace ctf evaluator wrapper."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
import shutil, tempfile
|
|
5
5
|
from pathlib import Path
|
|
@@ -17,4 +17,6 @@ with tempfile.TemporaryDirectory(prefix='ctf-eval-') as td:
|
|
|
17
17
|
if not result.get('ok'):
|
|
18
18
|
raise SystemExit(fail({'task': 'ctf', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
|
|
19
19
|
row = candidate_row(result['summary'])
|
|
20
|
-
|
|
20
|
+
if not row:
|
|
21
|
+
raise SystemExit(fail({'task': 'ctf', 'ok': False, 'error': 'candidate row missing from evaluator summary'}))
|
|
22
|
+
emit({'task': 'ctf', 'ok': True, 'score': row.get('score', row.get('rating', 0)), 'rating': row.get('rating', row.get('theta', 0)), 'theta': row.get('theta', 0)})
|
package/eval/featurebench.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Single-workspace
|
|
2
|
+
"""Single-workspace FeatureBench evaluator wrapper."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
import os, tempfile
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from common import EVAL_ROOT, candidate_row, clean_copy, emit, fail, parse_workspace, run_legacy
|
|
6
|
+
from common import EVAL_ROOT, candidate_row, clean_copy, emit, fail, parse_workspace, restore_baseline, run_legacy, verify_venv
|
|
7
7
|
|
|
8
8
|
workspace = parse_workspace()
|
|
9
|
+
problem = verify_venv(workspace)
|
|
10
|
+
if problem is not None:
|
|
11
|
+
raise SystemExit(fail({'task': 'featurebench', 'ok': False, 'error': problem}))
|
|
9
12
|
with tempfile.TemporaryDirectory(prefix='feature-eval-') as td:
|
|
10
13
|
temp = Path(td)
|
|
11
14
|
candidate = temp / 'candidate' / 'workspace'
|
|
@@ -13,13 +16,14 @@ with tempfile.TemporaryDirectory(prefix='feature-eval-') as td:
|
|
|
13
16
|
clean_copy(workspace, candidate, ignore_venv=True)
|
|
14
17
|
venv = workspace / '.venv'
|
|
15
18
|
if venv.is_dir():
|
|
16
|
-
|
|
19
|
+
try:
|
|
20
|
+
os.symlink(venv, candidate / '.venv')
|
|
21
|
+
except FileExistsError:
|
|
22
|
+
pass
|
|
23
|
+
restore_baseline(candidate)
|
|
17
24
|
result = run_legacy(EVAL_ROOT / 'legacy' / 'featurebench.py', ['--submissions', str(temp)], temp / 'out')
|
|
18
25
|
if not result.get('ok'):
|
|
19
26
|
raise SystemExit(fail({'task': 'featurebench', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
|
|
20
27
|
row = candidate_row(result['summary'])
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
errors = row.get('errors', 0)
|
|
24
|
-
resolved = failed == 0 and errors == 0 and passed > 0
|
|
25
|
-
emit({'task': 'featurebench', 'ok': True, 'score': 100.0 if resolved else 0.0, 'resolved': resolved, 'passRate': row.get('score', 0), 'passed': passed, 'failed': failed, 'errors': errors, 'status': row.get('status')})
|
|
28
|
+
resolved = row.get('resolved') is True
|
|
29
|
+
emit({'task': 'featurebench', 'ok': True, 'score': 100.0 if resolved else 0.0, 'resolved': resolved, 'passRate': row.get('score', 0), 'passed': row.get('passed', 0), 'failed': row.get('failed', 0), 'errors': row.get('errors', 0), 'f2pPassed': row.get('f2p_passed', 0), 'f2pTotal': row.get('f2p_total', 0), 'status': row.get('status')})
|
package/eval/land.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Single-workspace
|
|
2
|
+
"""Single-workspace land evaluator wrapper."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
import shutil, tempfile
|
|
5
5
|
from pathlib import Path
|
|
@@ -17,4 +17,6 @@ with tempfile.TemporaryDirectory(prefix='land-eval-') as td:
|
|
|
17
17
|
if not result.get('ok'):
|
|
18
18
|
raise SystemExit(fail({'task': 'land', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
|
|
19
19
|
row = candidate_row(result['summary'])
|
|
20
|
-
|
|
20
|
+
if not row:
|
|
21
|
+
raise SystemExit(fail({'task': 'land', 'ok': False, 'error': 'candidate row missing from evaluator summary'}))
|
|
22
|
+
emit({'task': 'land', 'ok': True, 'score': row.get('score', row.get('rating', 0)), 'rating': row.get('rating', row.get('theta', 0)), 'theta': row.get('theta', 0)})
|
package/eval/legacy/bomberman.py
CHANGED
|
@@ -107,13 +107,16 @@ def main() -> int:
|
|
|
107
107
|
cmd = [sys.executable, "-X", "utf8", str(task / "tournament" / "run.py"), "--output", str(run_dir),
|
|
108
108
|
"--bots", *bots, "--sizes", *sizes, "--seeds", str(seeds), "--workers", str(max(1, args.workers))]
|
|
109
109
|
print("tier=%s sizes=%s seeds=%d bots=%d" % (args.tier, sizes, seeds, len(bots)), flush=True)
|
|
110
|
-
completed = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
110
|
+
completed = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3600)
|
|
111
111
|
if completed.returncode != 0:
|
|
112
112
|
print(completed.stdout[-4000:], completed.stderr[-4000:], file=sys.stderr)
|
|
113
113
|
return 2
|
|
114
114
|
ratings_doc = load_json(run_dir / "bt.json") or {}
|
|
115
|
+
if ratings_doc.get("unavailable"):
|
|
116
|
+
print("bomberman rating unavailable: " + str(ratings_doc.get("unavailable")), file=sys.stderr)
|
|
117
|
+
return 2
|
|
115
118
|
ratings = ratings_doc.get("ratings", {})
|
|
116
|
-
prices = load_json(ROOT / "prices.json") or {}
|
|
119
|
+
prices = load_json(ROOT / "bomberman-prices.json") or load_json(ROOT / "prices.json") or {}
|
|
117
120
|
|
|
118
121
|
rows = []
|
|
119
122
|
for name, program in programs.items():
|
package/eval/legacy/ctf.py
CHANGED
|
@@ -123,14 +123,17 @@ def main() -> int:
|
|
|
123
123
|
print("tier=%s sizes=%s maps=%d players=%d" % (args.tier, sizes, maps_per_size, len(players)), flush=True)
|
|
124
124
|
for step in steps:
|
|
125
125
|
completed = subprocess.run([sys.executable, "-X", "utf8", tournament, *step], capture_output=True, text=True,
|
|
126
|
-
encoding="utf-8", errors="replace")
|
|
126
|
+
encoding="utf-8", errors="replace", timeout=1800)
|
|
127
127
|
if completed.returncode != 0:
|
|
128
128
|
print(completed.stdout[-4000:], completed.stderr[-4000:], file=sys.stderr)
|
|
129
129
|
return 2
|
|
130
130
|
scores = load_json(games_dir / "成绩.json") or {}
|
|
131
|
+
if scores.get("unavailable"):
|
|
132
|
+
print("ctf rating unavailable: " + str(scores.get("unavailable")), file=sys.stderr)
|
|
133
|
+
return 2
|
|
131
134
|
raw_ratings = scores.get("ratings", {})
|
|
132
135
|
ratings = {name: (value[0] if isinstance(value, list) and value else value) for name, value in raw_ratings.items()}
|
|
133
|
-
prices = load_json(ROOT / "prices.json") or {}
|
|
136
|
+
prices = load_json(ROOT / "ctf-prices.json") or load_json(ROOT / "prices.json") or {}
|
|
134
137
|
|
|
135
138
|
rows = []
|
|
136
139
|
for name, program in players.items():
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
Submission layout:
|
|
5
5
|
submissions/<name>/workspace/ (copy of the agent workspace, including .venv)
|
|
6
6
|
|
|
7
|
-
The hidden feature test file is copied from the plugin asset; scoring
|
|
8
|
-
|
|
7
|
+
The hidden feature test file is copied from the plugin asset; scoring checks the
|
|
8
|
+
fail-to-pass file and every pass-to-pass file explicitly (a skipped or removed
|
|
9
|
+
test does not count as resolved).
|
|
9
10
|
"""
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
@@ -23,6 +24,7 @@ ASSET = ROOT.parents[1] / "assets" / "featurebench"
|
|
|
23
24
|
PASSED = re.compile(r"(\d+) passed")
|
|
24
25
|
FAILED = re.compile(r"(\d+) failed")
|
|
25
26
|
ERRORS = re.compile(r"(\d+) error")
|
|
27
|
+
RESULT = re.compile(r"^(PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS) (\S+)", re.MULTILINE)
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def load_json(path: Path):
|
|
@@ -39,18 +41,29 @@ def find_workspace(directory: Path) -> Path | None:
|
|
|
39
41
|
return None
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def install_hidden_test(workspace: Path) ->
|
|
44
|
+
def install_hidden_test(workspace: Path) -> str:
|
|
43
45
|
source = ASSET / "hidden" / "test_regression.py"
|
|
44
|
-
if source.is_file():
|
|
45
|
-
|
|
46
|
+
if not source.is_file():
|
|
47
|
+
return "hidden regression test is missing"
|
|
48
|
+
shutil.copy2(source, workspace / "tests" / "test_regression.py")
|
|
49
|
+
return ""
|
|
46
50
|
|
|
47
51
|
|
|
48
|
-
def
|
|
52
|
+
def matches(node: str, selectors: list[str]) -> bool:
|
|
53
|
+
for selector in selectors:
|
|
54
|
+
if node == selector or node.startswith(selector + "::") or node.startswith(selector + "/"):
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run_tests(workspace: Path, instance: dict) -> tuple[int, int, int, str, dict]:
|
|
49
60
|
venv = workspace / ".venv" / "bin" / "python"
|
|
50
61
|
python = str(venv) if venv.is_file() else "python3"
|
|
51
|
-
|
|
62
|
+
fail_to_pass = list(instance.get("FAIL_TO_PASS", []))
|
|
63
|
+
pass_to_pass = list(instance.get("PASS_TO_PASS", []))
|
|
64
|
+
tests = fail_to_pass + pass_to_pass
|
|
52
65
|
env = {k: v for k, v in __import__("os").environ.items() if k not in {"PYTHONPATH", "PYTHONHOME"}}
|
|
53
|
-
completed = subprocess.run([python, "-m", "pytest", *tests, "-q", "--tb=no"], cwd=workspace,
|
|
66
|
+
completed = subprocess.run([python, "-m", "pytest", *tests, "-q", "--tb=no", "-rA"], cwd=workspace,
|
|
54
67
|
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=1800, env=env)
|
|
55
68
|
output = (completed.stdout or "") + "\n" + (completed.stderr or "")
|
|
56
69
|
passed = int(PASSED.search(output).group(1)) if PASSED.search(output) else 0
|
|
@@ -58,7 +71,31 @@ def run_tests(workspace: Path, instance: dict) -> tuple[int, int, int, str]:
|
|
|
58
71
|
errors = int(ERRORS.search(output).group(1)) if ERRORS.search(output) else 0
|
|
59
72
|
if completed.returncode != 0 and passed == 0 and failed == 0 and errors == 0:
|
|
60
73
|
errors = 1
|
|
61
|
-
|
|
74
|
+
outcomes = {node: status for status, node in RESULT.findall(output)}
|
|
75
|
+
f2p_total = f2p_passed = 0
|
|
76
|
+
p2p_total = p2p_passed = 0
|
|
77
|
+
for node, status in outcomes.items():
|
|
78
|
+
if matches(node, fail_to_pass):
|
|
79
|
+
f2p_total += 1
|
|
80
|
+
f2p_passed += 1 if status == "PASSED" else 0
|
|
81
|
+
elif matches(node, pass_to_pass):
|
|
82
|
+
p2p_total += 1
|
|
83
|
+
p2p_passed += 1 if status == "PASSED" else 0
|
|
84
|
+
resolved = (
|
|
85
|
+
len(fail_to_pass) > 0
|
|
86
|
+
and f2p_total > 0
|
|
87
|
+
and f2p_passed == f2p_total
|
|
88
|
+
and p2p_total > 0
|
|
89
|
+
and p2p_passed == p2p_total
|
|
90
|
+
and completed.returncode == 0
|
|
91
|
+
)
|
|
92
|
+
return passed, failed, errors, output[-3000:], {
|
|
93
|
+
"resolved": resolved,
|
|
94
|
+
"f2p_total": f2p_total,
|
|
95
|
+
"f2p_passed": f2p_passed,
|
|
96
|
+
"p2p_total": p2p_total,
|
|
97
|
+
"p2p_passed": p2p_passed,
|
|
98
|
+
}
|
|
62
99
|
|
|
63
100
|
|
|
64
101
|
def main() -> int:
|
|
@@ -78,20 +115,23 @@ def main() -> int:
|
|
|
78
115
|
for directory in directories:
|
|
79
116
|
workspace = find_workspace(directory)
|
|
80
117
|
if workspace is None:
|
|
81
|
-
rows.append({"name": directory.name, "status": "missing-workspace", "score": 0.0, "passed": 0, "failed": 0, "errors": 0})
|
|
118
|
+
rows.append({"name": directory.name, "status": "missing-workspace", "score": 0.0, "passed": 0, "failed": 0, "errors": 0, "resolved": False})
|
|
82
119
|
continue
|
|
83
120
|
try:
|
|
84
|
-
install_hidden_test(workspace)
|
|
85
|
-
|
|
121
|
+
hidden_error = install_hidden_test(workspace)
|
|
122
|
+
if hidden_error:
|
|
123
|
+
rows.append({"name": directory.name, "status": "hidden-test-failed", "score": 0.0, "passed": 0, "failed": 0, "errors": 1, "resolved": False, "detail": hidden_error})
|
|
124
|
+
continue
|
|
125
|
+
passed, failed, errors, detail, extra = run_tests(workspace, instance)
|
|
86
126
|
total = passed + failed + errors
|
|
87
127
|
score = 100.0 * passed / total if total else 0.0
|
|
88
|
-
rows.append({"name": directory.name, "status": "ok", "score": round(score, 2), "passed": passed, "failed": failed, "errors": errors, "detail": detail})
|
|
128
|
+
rows.append({"name": directory.name, "status": "ok", "score": round(score, 2), "passed": passed, "failed": failed, "errors": errors, "detail": detail, **extra})
|
|
89
129
|
except Exception as error:
|
|
90
|
-
rows.append({"name": directory.name, "status": "error", "score": 0.0, "passed": 0, "failed": 0, "errors": 1, "detail": str(error)})
|
|
130
|
+
rows.append({"name": directory.name, "status": "error", "score": 0.0, "passed": 0, "failed": 0, "errors": 1, "resolved": False, "detail": str(error)})
|
|
91
131
|
rows.sort(key=lambda row: (-row["score"], row["name"]))
|
|
92
132
|
payload = {"generatedAt": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "benchmark": "featurebench", "instance": instance.get("instance_id"), "results": rows}
|
|
93
133
|
(out / "summary.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
94
|
-
lines = ["# FeatureBench · 单 feature 排行", "", "- 实例:%s" % instance.get("instance_id", ""), "- 指标:隐藏
|
|
134
|
+
lines = ["# FeatureBench · 单 feature 排行", "", "- 实例:%s" % instance.get("instance_id", ""), "- 指标:隐藏 feature 测试与全部回归文件通过才 resolved", "", "| 名次 | 名称 | 得分 | 通过 | 失败 | 错误 | 状态 |", "|---:|---|---:|---:|---:|---:|---|"]
|
|
95
135
|
for rank, row in enumerate(rows, 1):
|
|
96
136
|
lines.append("| %d | %s | %.1f | %d | %d | %d | %s |" % (rank, row["name"], row["score"], row["passed"], row["failed"], row["errors"], row["status"]))
|
|
97
137
|
(out / "summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
package/eval/legacy/land.py
CHANGED
|
@@ -112,20 +112,23 @@ def main() -> int:
|
|
|
112
112
|
if melee == 0:
|
|
113
113
|
cmd += ["--only", "duel"]
|
|
114
114
|
print("tier=%s maps=%d melee=%d entries=%d" % (args.tier, maps, melee, len(list(entries.iterdir()))), flush=True)
|
|
115
|
-
completed = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
115
|
+
completed = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=1800)
|
|
116
116
|
if completed.returncode != 0:
|
|
117
117
|
print(completed.stdout[-4000:], completed.stderr[-4000:], file=sys.stderr)
|
|
118
118
|
return 2
|
|
119
119
|
ratings_prefix = out / "ratings"
|
|
120
120
|
rate = subprocess.run([sys.executable, "-X", "utf8", str(task / "tournament" / "rate.py"), str(results_jsonl),
|
|
121
121
|
"--entries", str(entries), "--out", str(ratings_prefix)], capture_output=True, text=True,
|
|
122
|
-
encoding="utf-8", errors="replace")
|
|
122
|
+
encoding="utf-8", errors="replace", timeout=1800)
|
|
123
123
|
if rate.returncode != 0:
|
|
124
124
|
print(rate.stdout[-4000:], rate.stderr[-4000:], file=sys.stderr)
|
|
125
125
|
return 2
|
|
126
126
|
ratings = load_json(Path(str(ratings_prefix) + ".json")) or {}
|
|
127
|
+
if ratings.get("unavailable"):
|
|
128
|
+
print("land rating unavailable: " + str(ratings.get("unavailable")), file=sys.stderr)
|
|
129
|
+
return 2
|
|
127
130
|
models = ratings.get("models", {})
|
|
128
|
-
prices = load_json(ROOT / "prices.json") or {}
|
|
131
|
+
prices = load_json(ROOT / "land-prices.json") or load_json(ROOT / "prices.json") or {}
|
|
129
132
|
|
|
130
133
|
rows = []
|
|
131
134
|
all_names = list(names) + [name for name in models if name not in names]
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
Submission layout:
|
|
5
5
|
submissions/<name>/workspace/ (copy of the agent workspace, including .venv)
|
|
6
6
|
|
|
7
|
-
The hidden test patch is applied from the plugin asset; scoring
|
|
8
|
-
instance's fail-to-pass and pass-to-pass
|
|
7
|
+
The hidden test patch is applied from the plugin asset; scoring checks the
|
|
8
|
+
instance's fail-to-pass and pass-to-pass node ids explicitly (a skipped or
|
|
9
|
+
renamed fail-to-pass test does not count as resolved).
|
|
9
10
|
"""
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
@@ -22,6 +23,7 @@ ASSET = ROOT.parents[1] / "assets" / "swe-bench-pro"
|
|
|
22
23
|
PASSED = re.compile(r"(\d+) passed")
|
|
23
24
|
FAILED = re.compile(r"(\d+) failed")
|
|
24
25
|
ERRORS = re.compile(r"(\d+) error")
|
|
26
|
+
RESULT = re.compile(r"^(PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS) (\S+)", re.MULTILINE)
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def load_json(path: Path):
|
|
@@ -48,22 +50,34 @@ def find_workspace(directory: Path) -> Path | None:
|
|
|
48
50
|
return None
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
def apply_hidden_tests(workspace: Path) ->
|
|
53
|
+
def apply_hidden_tests(workspace: Path) -> str:
|
|
52
54
|
patch = ASSET / "hidden" / "test.patch"
|
|
53
55
|
if not patch.is_file():
|
|
54
|
-
return
|
|
55
|
-
subprocess.run(["patch", "-p1", "--forward", "-i", str(patch)], cwd=workspace,
|
|
56
|
-
|
|
56
|
+
return "hidden test patch is missing"
|
|
57
|
+
completed = subprocess.run(["patch", "-p1", "--forward", "-i", str(patch)], cwd=workspace,
|
|
58
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
59
|
+
if completed.returncode != 0:
|
|
60
|
+
return "hidden test patch failed to apply: " + (completed.stdout + completed.stderr)[-2000:]
|
|
61
|
+
return ""
|
|
57
62
|
|
|
58
63
|
|
|
59
|
-
def
|
|
64
|
+
def matches(node: str, selectors: list[str]) -> bool:
|
|
65
|
+
for selector in selectors:
|
|
66
|
+
if node == selector or node.startswith(selector + "::") or node.startswith(selector + "/"):
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_tests(workspace: Path, instance: dict) -> tuple[int, int, int, str, dict]:
|
|
60
72
|
venv = workspace / ".venv" / "bin" / "python"
|
|
61
73
|
python = str(venv) if venv.is_file() else "python3"
|
|
62
|
-
|
|
74
|
+
fail_to_pass = as_list(instance.get("fail_to_pass"))
|
|
75
|
+
pass_to_pass = as_list(instance.get("pass_to_pass"))
|
|
76
|
+
tests = fail_to_pass + pass_to_pass
|
|
63
77
|
if not tests:
|
|
64
78
|
tests = as_list(instance.get("selected_test_files_to_run"))
|
|
65
79
|
env = {k: v for k, v in __import__("os").environ.items() if k not in {"PYTHONPATH", "PYTHONHOME"}}
|
|
66
|
-
completed = subprocess.run([python, "-m", "pytest", *tests, "-q", "--tb=no"], cwd=workspace,
|
|
80
|
+
completed = subprocess.run([python, "-m", "pytest", *tests, "-q", "--tb=no", "-rA"], cwd=workspace,
|
|
67
81
|
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=1800, env=env)
|
|
68
82
|
output = (completed.stdout or "") + "\n" + (completed.stderr or "")
|
|
69
83
|
passed = int(PASSED.search(output).group(1)) if PASSED.search(output) else 0
|
|
@@ -71,7 +85,31 @@ def run_tests(workspace: Path, instance: dict) -> tuple[int, int, int, str]:
|
|
|
71
85
|
errors = int(ERRORS.search(output).group(1)) if ERRORS.search(output) else 0
|
|
72
86
|
if completed.returncode != 0 and passed == 0 and failed == 0 and errors == 0:
|
|
73
87
|
errors = 1
|
|
74
|
-
|
|
88
|
+
outcomes = {node: status for status, node in RESULT.findall(output)}
|
|
89
|
+
f2p_total = f2p_passed = 0
|
|
90
|
+
p2p_total = p2p_passed = 0
|
|
91
|
+
for node, status in outcomes.items():
|
|
92
|
+
if matches(node, fail_to_pass):
|
|
93
|
+
f2p_total += 1
|
|
94
|
+
f2p_passed += 1 if status == "PASSED" else 0
|
|
95
|
+
elif matches(node, pass_to_pass):
|
|
96
|
+
p2p_total += 1
|
|
97
|
+
p2p_passed += 1 if status == "PASSED" else 0
|
|
98
|
+
resolved = (
|
|
99
|
+
len(fail_to_pass) > 0
|
|
100
|
+
and f2p_total > 0
|
|
101
|
+
and f2p_passed == f2p_total
|
|
102
|
+
and p2p_total > 0
|
|
103
|
+
and p2p_passed == p2p_total
|
|
104
|
+
and completed.returncode == 0
|
|
105
|
+
)
|
|
106
|
+
return passed, failed, errors, output[-3000:], {
|
|
107
|
+
"resolved": resolved,
|
|
108
|
+
"f2p_total": f2p_total,
|
|
109
|
+
"f2p_passed": f2p_passed,
|
|
110
|
+
"p2p_total": p2p_total,
|
|
111
|
+
"p2p_passed": p2p_passed,
|
|
112
|
+
}
|
|
75
113
|
|
|
76
114
|
|
|
77
115
|
def main() -> int:
|
|
@@ -91,20 +129,23 @@ def main() -> int:
|
|
|
91
129
|
for directory in directories:
|
|
92
130
|
workspace = find_workspace(directory)
|
|
93
131
|
if workspace is None:
|
|
94
|
-
rows.append({"name": directory.name, "status": "missing-workspace", "score": 0.0, "passed": 0, "failed": 0, "errors": 0})
|
|
132
|
+
rows.append({"name": directory.name, "status": "missing-workspace", "score": 0.0, "passed": 0, "failed": 0, "errors": 0, "resolved": False})
|
|
95
133
|
continue
|
|
96
134
|
try:
|
|
97
|
-
apply_hidden_tests(workspace)
|
|
98
|
-
|
|
135
|
+
patch_error = apply_hidden_tests(workspace)
|
|
136
|
+
if patch_error:
|
|
137
|
+
rows.append({"name": directory.name, "status": "hidden-test-failed", "score": 0.0, "passed": 0, "failed": 0, "errors": 1, "resolved": False, "detail": patch_error})
|
|
138
|
+
continue
|
|
139
|
+
passed, failed, errors, detail, extra = run_tests(workspace, instance)
|
|
99
140
|
total = passed + failed + errors
|
|
100
141
|
score = 100.0 * passed / total if total else 0.0
|
|
101
|
-
rows.append({"name": directory.name, "status": "ok", "score": round(score, 2), "passed": passed, "failed": failed, "errors": errors, "detail": detail})
|
|
142
|
+
rows.append({"name": directory.name, "status": "ok", "score": round(score, 2), "passed": passed, "failed": failed, "errors": errors, "detail": detail, **extra})
|
|
102
143
|
except Exception as error:
|
|
103
|
-
rows.append({"name": directory.name, "status": "error", "score": 0.0, "passed": 0, "failed": 0, "errors": 1, "detail": str(error)})
|
|
144
|
+
rows.append({"name": directory.name, "status": "error", "score": 0.0, "passed": 0, "failed": 0, "errors": 1, "resolved": False, "detail": str(error)})
|
|
104
145
|
rows.sort(key=lambda row: (-row["score"], row["name"]))
|
|
105
146
|
payload = {"generatedAt": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "benchmark": "swe-bench-pro", "instance": instance.get("instance_id"), "results": rows}
|
|
106
147
|
(out / "summary.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
107
|
-
lines = ["# SWE-Bench Pro · 单任务排行", "", "- 实例:%s" % instance.get("instance_id", ""), "- 指标:隐藏 fail-to-pass + pass-to-pass
|
|
148
|
+
lines = ["# SWE-Bench Pro · 单任务排行", "", "- 实例:%s" % instance.get("instance_id", ""), "- 指标:隐藏 fail-to-pass + pass-to-pass 节点全部通过才 resolved", "", "| 名次 | 名称 | 得分 | 通过 | 失败 | 错误 | 状态 |", "|---:|---|---:|---:|---:|---:|---|"]
|
|
108
149
|
for rank, row in enumerate(rows, 1):
|
|
109
150
|
lines.append("| %d | %s | %.1f | %d | %d | %d | %s |" % (rank, row["name"], row["score"], row["passed"], row["failed"], row["errors"], row["status"]))
|
|
110
151
|
(out / "summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
package/eval/legacy/tank.py
CHANGED
|
@@ -258,8 +258,8 @@ def main() -> int:
|
|
|
258
258
|
if not directories:
|
|
259
259
|
print("no submissions found under " + str(submissions_root), file=sys.stderr)
|
|
260
260
|
return 2
|
|
261
|
-
prices_path = ROOT / "prices.json"
|
|
262
|
-
prices = load_json(prices_path) or {}
|
|
261
|
+
prices_path = ROOT / "tank-prices.json"
|
|
262
|
+
prices = load_json(prices_path) or load_json(ROOT / "prices.json") or {}
|
|
263
263
|
|
|
264
264
|
participants: list[Participant] = [Participant("baseline", workspace / "baseline.cpp", "baseline")]
|
|
265
265
|
participants += [load_submission(directory) for directory in directories]
|
package/eval/legacy/warehouse.py
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"""Independent evaluation for dsh-eval-guard warehouse submissions.
|
|
3
3
|
|
|
4
4
|
Reads submissions/<name>/{src/*.cpp,run/,cost.json}, compiles with the fixed
|
|
5
|
-
g++ flags, compares every public workload byte-for-byte
|
|
6
|
-
output, measures CPU time against the baseline, and writes a
|
|
7
|
-
cost/violation/static-scan reports.
|
|
5
|
+
g++ flags, compares every public workload byte-for-byte (CRLF normalized) with
|
|
6
|
+
the expected output, measures CPU time against the baseline, and writes a
|
|
7
|
+
leaderboard plus cost/violation/static-scan reports.
|
|
8
8
|
|
|
9
9
|
The evaluator is independent of the DSH plugin: copy the run/ directory from
|
|
10
10
|
$DSH_HOME/eval-guard/runs/<run-id>/ next to src/.
|
|
@@ -220,7 +220,7 @@ def main() -> int:
|
|
|
220
220
|
out_dir = Path(args.out).resolve() if args.out else ROOT / "results" / time.strftime("%Y%m%d-%H%M%S")
|
|
221
221
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
222
222
|
build_dir = out_dir / "build"
|
|
223
|
-
prices = load_json(ROOT / "prices.json") or {}
|
|
223
|
+
prices = load_json(ROOT / "warehouse-prices.json") or load_json(ROOT / "prices.json") or {}
|
|
224
224
|
|
|
225
225
|
print("tier=%s workloads=%s repeats=%d" % (args.tier, ",".join(names), repeats))
|
|
226
226
|
baseline = Participant("baseline", baseline_src, "baseline")
|