dsh-eval-guard 0.7.1 → 0.7.3

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.
Files changed (73) hide show
  1. package/ISSUES.md +235 -0
  2. package/README.md +46 -18
  3. package/assets/neural-cifar/workspace.zip +0 -0
  4. package/assets/neural-cpp/workspace.zip +0 -0
  5. package/assets/neural-fp32/workspace.zip +0 -0
  6. package/assets/neural-pendulum/SPEC.md +41 -38
  7. package/assets/neural-pendulum/TASK.md +146 -32
  8. package/assets/neural-pendulum/sim.cpp +4 -2
  9. package/assets/neural-pendulum/sim_eval +0 -0
  10. package/assets/neural-pendulum/workspace.zip +0 -0
  11. package/eval/README.md +19 -9
  12. package/eval/__pycache__/bomberman.cpython-310.pyc +0 -0
  13. package/eval/__pycache__/commit0.cpython-310.pyc +0 -0
  14. package/eval/__pycache__/common.cpython-310.pyc +0 -0
  15. package/eval/__pycache__/ctf.cpython-310.pyc +0 -0
  16. package/eval/__pycache__/featurebench.cpython-310.pyc +0 -0
  17. package/eval/__pycache__/land.cpython-310.pyc +0 -0
  18. package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
  19. package/eval/__pycache__/neural-cpp.cpython-310.pyc +0 -0
  20. package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
  21. package/eval/__pycache__/neural-pendulum.cpython-310.pyc +0 -0
  22. package/eval/__pycache__/swe-bench-pro.cpython-310.pyc +0 -0
  23. package/eval/__pycache__/tank.cpython-310.pyc +0 -0
  24. package/eval/__pycache__/warehouse.cpython-310.pyc +0 -0
  25. package/eval/bomberman.py +5 -3
  26. package/eval/commit0.py +10 -3
  27. package/eval/common.py +169 -5
  28. package/eval/ctf.py +4 -2
  29. package/eval/featurebench.py +12 -8
  30. package/eval/land.py +4 -2
  31. package/eval/legacy/__pycache__/bomberman.cpython-310.pyc +0 -0
  32. package/eval/legacy/__pycache__/commit0.cpython-310.pyc +0 -0
  33. package/eval/legacy/__pycache__/ctf.cpython-310.pyc +0 -0
  34. package/eval/legacy/__pycache__/featurebench.cpython-310.pyc +0 -0
  35. package/eval/legacy/__pycache__/land.cpython-310.pyc +0 -0
  36. package/eval/legacy/__pycache__/swe_bench_pro.cpython-310.pyc +0 -0
  37. package/eval/legacy/__pycache__/tank.cpython-310.pyc +0 -0
  38. package/eval/legacy/__pycache__/warehouse.cpython-310.pyc +0 -0
  39. package/eval/legacy/bomberman.py +5 -2
  40. package/eval/legacy/ctf.py +5 -2
  41. package/eval/legacy/featurebench.py +55 -15
  42. package/eval/legacy/land.py +6 -3
  43. package/eval/legacy/swe_bench_pro.py +57 -16
  44. package/eval/legacy/tank.py +2 -2
  45. package/eval/legacy/warehouse.py +4 -4
  46. package/eval/neural-cpp.py +9 -6
  47. package/eval/neural-fp32.py +17 -5
  48. package/eval/swe-bench-pro.py +11 -7
  49. package/eval/tank.py +11 -2
  50. package/eval/warehouse.py +17 -1
  51. package/lib/client/index.js +73 -46
  52. package/lib/client/index.js.map +1 -1
  53. package/lib/index.js +894 -301
  54. package/native/dsh-eval-supervisor +0 -0
  55. package/native/supervisor.c +190 -26
  56. package/neural-cifar-eval/README.md +5 -0
  57. package/neural-cifar-eval/__pycache__/cifar_cache.cpython-310.pyc +0 -0
  58. package/neural-cifar-eval/__pycache__/common.cpython-310.pyc +0 -0
  59. package/neural-cifar-eval/__pycache__/fetch_cifar.cpython-310.pyc +0 -0
  60. package/neural-cifar-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
  61. package/neural-cifar-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
  62. package/neural-cifar-eval/common.py +13 -0
  63. package/neural-cifar-eval/ops_eval.py +17 -5
  64. package/neural-cifar-eval/run_eval.py +42 -32
  65. package/neural-pendulum-eval/README.md +62 -0
  66. package/neural-pendulum-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
  67. package/neural-pendulum-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
  68. package/neural-pendulum-eval/ops_eval.py +603 -0
  69. package/neural-pendulum-eval/run_eval.py +218 -137
  70. package/neural-pendulum-eval/sim_eval +0 -0
  71. package/package.json +7 -11
  72. package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
  73. package/assets/neural-pendulum/include/nn/nn.hpp +0 -88
@@ -6,11 +6,11 @@
6
6
 
7
7
  目标:
8
8
 
9
- - 从三杆全部向下的固定初始状态起摆
9
+ - 训练时从随机小扰动、评测时从固定初始状态起摆
10
10
  - 保持三根杆全部向上至少 t_tgt = 10.0 秒
11
- - 同时保证神经网络基础设施通过固定头文件契约并通过隐藏测试
11
+ - 同时保证神经网络基础设施通过 build/nn_task 固定 CLI 的数值评测
12
12
 
13
- 允许使用 PyTorch 做开发阶段的自查和数值对照。最终提交源码中不得出现 torch,评测环境不提供 Python/torch。
13
+ 开发阶段可以使用 Python/PyTorch 做离线数值对照和实验;最终提交的基础设施和控制器源码必须是纯 C++,不得出现 torch,候选运行时不提供 Python/torch。
14
14
 
15
15
  ## 环境
16
16
 
@@ -100,7 +100,7 @@ t_can = 三根杆同时处于全向上容差内的最长连续时间
100
100
 
101
101
  ~~~
102
102
  t_can >= t_tgt
103
- 且基础设施测试全部通过
103
+ 且神经网络基础设施数值评测全部通过
104
104
  ~~~
105
105
 
106
106
  任务完成,计时停止。
@@ -152,34 +152,140 @@ cmake --build build -j4
152
152
  必须产出:
153
153
 
154
154
  ~~~
155
- build/libnn.a
155
+ build/nn_task
156
156
  build/control
157
157
  ~~~
158
158
 
159
- ### 1. 基础设施入口
159
+ 工作区模板 CMakeLists.txt 默认从 `src/core/*.cpp` 构建共享静态库,从 `src/nn_task.cpp` 和 `src/control.cpp` 构建两个可执行文件。可以按需调整目录结构和 CMakeLists.txt,但必须从零构建出上面两个二进制。
160
160
 
161
- 基础设施实现放在 `src/nn/*.cpp`,由 CMake 构建为 `build/libnn.a`;公开头文件 `include/nn/nn.hpp` 是固定 API 契约,不得修改声明或语义。
161
+ ### 1. 神经网络基础设施入口 build/nn_task
162
162
 
163
- 评测侧会独立编译隐藏测试并链接 `build/libnn.a`,不使用你写的 `nn_selftest` 作为验证依据。`build/nn_selftest` 可选,仅用于你自己的开发自测。
163
+ 基础设施通过固定 CLI 暴露,评测会逐操作调用。所有输入/输出文件都是连续 row-major float32,所有数值输出也必须写为 float32。
164
164
 
165
- 必须包含的基础设施:
165
+ #### 逐元素
166
166
 
167
- - fp32 tensor,形状检查与广播
168
- - elementwise add/sub/mul/div
169
- - matmul / linear
170
- - relu / softmax
171
- - layernorm
172
- - conv2d,支持 stride/padding/dilation/groups
173
- - MSE
174
- - matmul/linear/relu/layernorm/conv2d/MSE 反向
175
- - SGD momentum
176
- - AdamW
177
- - 模型保存/加载
178
- - 线程池
167
+ ~~~bash
168
+ build/nn_task elementwise --op add|sub|mul|div --shape 64,256 --a A --b B --out O
169
+ ~~~
170
+
171
+ 支持广播:从尾部对齐,维度为 1 可广播。
172
+
173
+ #### 矩阵乘法
174
+
175
+ ~~~bash
176
+ build/nn_task matmul --a-shape M,K --b-shape K,N --a A --b B --out O
177
+ ~~~
178
+
179
+ 只要求 2D 输入。
180
+
181
+ #### Linear
182
+
183
+ ~~~bash
184
+ build/nn_task linear --x-shape B,IN --w-shape OUT,IN --b-shape OUT --x X --w W --bias B --out O
185
+ ~~~
186
+
187
+ 计算 `O = X @ W^T + bias`。
188
+
189
+ #### 激活
190
+
191
+ ~~~bash
192
+ build/nn_task relu --shape N --x X --out O
193
+ build/nn_task softmax --shape B,C --x X --out O
194
+ ~~~
179
195
 
180
- 占位符或空实现会被判为基础设施错误。
196
+ Softmax 沿最后一维。
181
197
 
182
- 隐藏测试由评测侧注入,使用固定种子和独立数据,不复制到工作区。
198
+ #### LayerNorm
199
+
200
+ ~~~bash
201
+ build/nn_task layernorm --shape B,C --x X --gamma G --beta B --eps 1e-5 --out O
202
+ ~~~
203
+
204
+ 对最后一维归一化;gamma/beta 形状为 `C`。
205
+
206
+ #### MSE
207
+
208
+ ~~~bash
209
+ build/nn_task mse --pred-shape S --target-shape S --pred P --target T --out O
210
+ ~~~
211
+
212
+ 输出 1 个 float32,为所有元素平均平方误差。
213
+
214
+ #### Conv2d
215
+
216
+ ~~~bash
217
+ build/nn_task conv2d --x-shape N,C,H,W --w-shape O,Cg,KH,KW --b-shape O --x X --w W --bias B --stride SH,SW --padding PH,PW --dilation DH,DW --groups G --out OUT
218
+ ~~~
219
+
220
+ 标准 NCHW 卷积,`Cg = C / groups`。
221
+
222
+ #### 反向传播
223
+
224
+ ~~~bash
225
+ build/nn_task matmul-backward --a-shape M,K --b-shape K,N --a A --b B --grad G --da DA --db DB
226
+ build/nn_task linear-backward --x-shape B,IN --w-shape OUT,IN --b-shape OUT --x X --w W --grad G --dx DX --dw DW --db DB
227
+ build/nn_task relu-backward --shape N --x X --grad G --dx DX
228
+ build/nn_task layernorm-backward --shape B,C --x X --gamma G --grad G --eps 1e-5 --dx DX --dgamma DG
229
+ build/nn_task mse-backward --pred-shape S --target-shape S --pred P --target T --grad G --dp DP --dtarget DT
230
+ build/nn_task conv2d-backward --x-shape N,C,H,W --w-shape O,Cg,KH,KW --b-shape O --x X --w W --grad G --stride SH,SW --padding PH,PW --dilation DH,DW --groups G --dx DX --dw DW --db DB
231
+ ~~~
232
+
233
+ 反向传播定义为:上游梯度 `G` 对输入、权重、bias 的梯度;`G` 与 forward 输出同形状。
234
+
235
+ #### 优化器
236
+
237
+ ~~~bash
238
+ build/nn_task adamw --param P --grad G --m M --v V --lr LR --beta1 B1 --beta2 B2 --eps EPS --weight-decay WD --step T --out-param P2 --out-m M2 --out-v V2
239
+ build/nn_task sgd-momentum --param P --grad G --momentum-buffer MB --lr LR --momentum MU --out-param P2 --out-buffer MB2
240
+ ~~~
241
+
242
+ AdamW 使用 decoupled weight decay:
243
+
244
+ ~~~text
245
+ m_t = beta1*m + (1-beta1)*g
246
+ v_t = beta2*v + (1-beta2)*g*g
247
+ m_hat = m_t / (1-beta1^t)
248
+ v_hat = v_t / (1-beta2^t)
249
+ param = param*(1-lr*wd) - lr*m_hat/(sqrt(v_hat)+eps)
250
+ ~~~
251
+
252
+ SGD momentum:
253
+
254
+ ~~~text
255
+ buffer = momentum*buffer + grad
256
+ param = param - lr*buffer
257
+ ~~~
258
+
259
+ #### 数值评测方式
260
+
261
+ 评测侧在独立进程中生成数据,并用 PyTorch 在相同输入/参数上计算参考值,逐操作比较。工作区不会收到期望输出张量。
262
+
263
+ - 每个操作使用 3 组独立随机数据(固定种子),形状固定
264
+ - 误差指标为加权平均相对误差:
265
+
266
+ ~~~
267
+ mre = sum(|candidate - reference|) / sum(|reference|)
268
+ ~~~
269
+
270
+ - 每个操作有独立阈值,单位与 fp32 精度匹配:
271
+
272
+ | 操作 | 加权平均相对误差阈值 |
273
+ | --- | --- |
274
+ | elementwise.add / sub / mul / div | 1e-6 |
275
+ | relu | 1e-6 |
276
+ | mse | 1e-6 |
277
+ | sgd-momentum | 1e-6 |
278
+ | matmul / linear | 1e-5 |
279
+ | softmax / layernorm | 1e-5 |
280
+ | adamw | 1e-5 |
281
+ | conv2d | 1e-4 |
282
+ | matmul-backward / linear-backward / relu-backward / layernorm-backward / mse-backward / conv2d-backward | 1e-4 |
283
+
284
+ - 任一操作不可执行、崩溃、超时(单次调用 60 秒)、输出文件缺失或元素数不符,或 mre 超过阈值,都会判为基础设施失败(stage = infra)
285
+ - 速度(GFLOP/s)会同时报告,但速度不参与通过判定
286
+ - 不做完整训练,不要求模型保存/加载格式;控制器需要的权重可以放在源码目录或控制器自行打包
287
+
288
+ 评测数据在测试进程内生成,不随工作区提供。不要伪造输出:评测直接执行你的二进制并检查它写出的文件。
183
289
 
184
290
  ### 2. 控制器入口
185
291
 
@@ -222,7 +328,7 @@ eval_test
222
328
 
223
329
  ### eval_test
224
330
 
225
- 由宿主对最新提交快照进行构建和隐藏仿真评估,返回:
331
+ 由宿主对最新提交快照进行构建、算子数值评测和隐藏仿真评估,返回:
226
332
 
227
333
  ~~~text
228
334
  stage
@@ -243,9 +349,18 @@ controller
243
349
  simulation
244
350
  scoring
245
351
  completed
246
- busy
247
352
  ~~~
248
353
 
354
+ 阶段含义:
355
+
356
+ - no_submission:还没有提交快照
357
+ - compile:CMake 构建失败,或没有产出 build/nn_task / build/control
358
+ - infra:build/nn_task 算子数值评测失败(不可执行、崩溃、超时、输出缺失或误差超阈值)
359
+ - controller:控制器启动、协议、崩溃、超时或输出非法
360
+ - simulation:隐藏仿真推进异常、NaN 或超时
361
+ - scoring:测试有效,但 `t_can < 10.0`
362
+ - completed:基础设施和仿真均通过,且 `t_can >= 10.0`
363
+
249
364
  最终排行分数不会返回给你,只用于赛后排名。
250
365
 
251
366
  ## 完成条件
@@ -269,10 +384,9 @@ t_can >= 10.0
269
384
 
270
385
  - 只允许 C++17 + CMake + C++ 标准库
271
386
  - 仅 CPU,fp32
272
- - 提交源码中不得出现 torch
273
- - 评测环境无 Python/torch
387
+ - 提交源码中不得出现 torch;候选运行时不提供 Python/torch(评测侧会用 PyTorch 计算数值参考值)
274
388
  - 无网络
275
- - 不得修改仿真器或公开头文件 `include/nn/nn.hpp`
389
+ - 不得修改或替换 tools/sim;它只用于训练和自测
276
390
  - 不得提交编译产物
277
391
  - 赛后人工审核:控制器必须是神经网络,且必须实际调用你实现的神经网络基础设施;手写控制律、查表、固定轨迹或外部依赖会被判无效
278
392
 
@@ -352,9 +466,9 @@ T_done 是第一次返回 completed 的测试请求时间。
352
466
 
353
467
  ## 建议工作顺序
354
468
 
355
- 1. 阅读 `include/nn/nn.hpp` 契约与 tools/sim 协议
356
- 2. 先实现最小可用的 tensor/算子/反向/优化器
357
- 3. 用自测和 `include/nn/nn.hpp` 契约检查基础设施
358
- 4. 用 tools/sim 采集数据并训练控制器
469
+ 1. 阅读 build/nn_task 的 CLI 协议与 tools/sim 协议
470
+ 2. 先实现最小可用的 tensor/算子/反向/优化器/线程池,并让 build/nn_task 跑通
471
+ 3. 用 PyTorch 或独立 double 标量参考、有限差分梯度检查逐算子核对误差,边界形状/广播/groups/stride/dilation 都要覆盖
472
+ 4. 用 tools/sim 采集数据并训练控制器,导出权重供 build/control 使用
359
473
  5. 反复提交和测试,优先把 t_can 提升到 10 秒
360
474
  6. 完成后立即总结
@@ -70,9 +70,11 @@ static void deriv(const State& s, const std::array<double,N>& tau, State& out) {
70
70
  G[i]=GRAV*std::sin(s.theta[i])*GCOEF[i];
71
71
  }
72
72
  // joint damping: b * D^T D omega, D lower bidiagonal with -1 previous
73
+ std::array<double,N> edge{};
74
+ for(int i=0;i<N;i++) edge[i] = s.omega[i] - (i>0?s.omega[i-1]:0.0);
73
75
  for(int i=0;i<N;i++){
74
- double e=s.omega[i] - (i>0?s.omega[i-1]:0.0);
75
- Q[i] -= DAMP*e;
76
+ double e = edge[i] - (i+1<N ? edge[i+1] : 0.0);
77
+ Q[i] = -DAMP*e;
76
78
  }
77
79
  for(int i=0;i<N;i++) rhs[i]=tau[i]-C[i]-G[i]+Q[i];
78
80
  solve3(M, rhs, out.omega);
Binary file
package/eval/README.md CHANGED
@@ -4,15 +4,25 @@
4
4
 
5
5
  | 任务 | 脚本 | 评分 |
6
6
  |---|---|---|
7
- | tank | eval/tank.py | 官方引擎 + BT,对基线 |
8
- | warehouse | eval/warehouse.py | 正确性 + 相对基线加速比 |
9
- | land | eval/land.py | 官方 round_robin/rate |
10
- | bomberman | eval/bomberman.py | 官方 tournament/run |
11
- | ctf | eval/ctf.py | 官方 arena plan/run/rate |
12
- | commit0 | eval/commit0.py | 单元测试通过率 |
13
- | swe-bench-pro | eval/swe-bench-pro.py | 隐藏 F2P/P2P 测试 |
14
- | featurebench | eval/featurebench.py | 隐藏 F2P/P2P 测试;全部通过才 resolved |
7
+ | tank | eval/tank.py | 官方引擎 + BT,对基线(quick 20 局) |
8
+ | warehouse | eval/warehouse.py | 正确性 + 相对基线加速比(quick 仅 pub_small) |
9
+ | land | eval/land.py | 官方 round_robin/rate(quick 仅单挑 2 图) |
10
+ | bomberman | eval/bomberman.py | 官方 tournament/run(quick 仅 2 人 1 种子) |
11
+ | ctf | eval/ctf.py | 官方 arena plan/run/rate(quick 仅 2/3 方) |
12
+ | commit0 | eval/commit0.py | 单元测试通过率(测试来自安装快照) |
13
+ | swe-bench-pro | eval/swe-bench-pro.py | 隐藏 F2P/P2P 节点全部通过才 resolved |
14
+ | featurebench | eval/featurebench.py | 隐藏 feature 测试 + 全部回归文件通过才 resolved |
15
+ | neural-cpp | eval/neural-cpp.py | 隐藏 C++ 测试;accuracy / matrixAccuracy / speedGeomean / score |
16
+ | neural-fp32 | eval/neural-fp32.py | 逐操作 MRE/阈值/速度;无统一 score |
17
+ | neural-cifar | eval/neural-cifar.py | FP32 算子指标 + 隐藏验证集准确率/FPS(独立目录) |
18
+ | neural-pendulum | eval/neural-pendulum.py | FP32 算子指标 + 固定初态控制 t_can(独立目录) |
15
19
 
16
20
  eval/legacy/ 保存从独立评测目录迁移过来的评测器;eval/arena/ 保存官方引擎、裁判和历史对手。common.py 负责临时工作区、结果解析和结果文件写入。
17
21
 
18
- 后台任务由 host 的 JobManager 执行,日志和结果保存在 $DSH_HOME/eval-guard/jobs/<job-id>/。
22
+ ## 完整性约束(宿主注入)
23
+
24
+ - `EVAL_BASELINE_DIR`:安装成功时快照的测试/配置目录。commit0/SWE/FeatureBench 的 wrapper 会在评测前用它覆盖候选副本,并删除候选新增的 `conftest.py`;同时校验 `.venv` 指纹(`venv.json`)。
25
+ - `EVAL_RESULT_FILE` / `EVAL_JOB_DIR`:只由顶层 evaluator 使用;所有 evaluator 再启动子进程时会剥掉这些变量,候选二进制拿不到结果文件路径。
26
+ - 隐藏资产(neural-cpp 的隐藏测试、CIFAR 的验证标签)在候选构建/推理开始前读入内存;CIFAR 的图像副本会放到独立临时目录,标签不落盘到候选可见路径。
27
+
28
+ 后台任务由 host 的 JobManager 执行,日志和结果保存在 $DSH_HOME/eval-guard/jobs/<job-id>/。Job 子进程的环境变量已收敛为白名单(PATH/HOME/LANG/LC_*/TMPDIR/SSL_* 等)。
package/eval/bomberman.py CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env python3
2
- """Single-workspace tank evaluator wrapper."""
2
+ """Single-workspace bomberman evaluator wrapper."""
3
3
  from __future__ import annotations
4
4
  import shutil, tempfile
5
5
  from pathlib import Path
6
6
  from common import EVAL_ROOT, candidate_row, emit, fail, parse_workspace, run_legacy
7
7
 
8
8
  workspace = parse_workspace()
9
- with tempfile.TemporaryDirectory(prefix='bomber-eval-') as td:
9
+ with tempfile.TemporaryDirectory(prefix='bomberman-eval-') as td:
10
10
  temp = Path(td)
11
11
  bot = workspace / 'submission' / 'bot.cpp'
12
12
  if not bot.is_file():
@@ -17,4 +17,6 @@ with tempfile.TemporaryDirectory(prefix='bomber-eval-') as td:
17
17
  if not result.get('ok'):
18
18
  raise SystemExit(fail({'task': 'bomberman', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
19
19
  row = candidate_row(result['summary'])
20
- emit({'task': 'bomberman', 'ok': True, 'score': row.get('score', row.get('rating', row.get('theta', 0))), 'rating': row.get('rating', row.get('theta', 0)), 'wins': row.get('wins', 0), 'losses': row.get('losses', 0), 'draws': row.get('draws', 0)})
20
+ if not row:
21
+ raise SystemExit(fail({'task': 'bomberman', 'ok': False, 'error': 'candidate row missing from evaluator summary'}))
22
+ emit({'task': 'bomberman', '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/commit0.py CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env python3
2
2
  """Single-workspace Commit0 evaluator wrapper."""
3
3
  from __future__ import annotations
4
- import os, shutil, tempfile
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': 'commit0', 'ok': False, 'error': problem}))
9
12
  with tempfile.TemporaryDirectory(prefix='commit0-eval-') as td:
10
13
  temp = Path(td)
11
14
  candidate = temp / 'candidate' / 'workspace'
@@ -13,7 +16,11 @@ with tempfile.TemporaryDirectory(prefix='commit0-eval-') as td:
13
16
  clean_copy(workspace, candidate, ignore_venv=True)
14
17
  venv = workspace / '.venv'
15
18
  if venv.is_dir():
16
- os.symlink(venv, candidate / '.venv')
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' / 'commit0.py', ['--submissions', str(temp)], temp / 'out')
18
25
  if not result.get('ok'):
19
26
  raise SystemExit(fail({'task': 'commit0', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
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) -> None:
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 ignore_venv and Path(directory) == source:
34
- return {name for name in names if name == '.venv'}
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
- env = {k: v for k, v in os.environ.items() if k not in {'PYTHONPATH', 'PYTHONHOME'}}
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 tank evaluator wrapper."""
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
- emit({'task': 'ctf', 'ok': True, 'score': row.get('score', row.get('rating', row.get('theta', 0))), 'rating': row.get('rating', row.get('theta', 0)), 'wins': row.get('wins', 0), 'losses': row.get('losses', 0), 'draws': row.get('draws', 0)})
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)})
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env python3
2
- """Single-workspace SWE-Bench Pro evaluator wrapper."""
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
- os.symlink(venv, candidate / '.venv')
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
- passed = row.get('passed', 0)
22
- failed = row.get('failed', 0)
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 tank evaluator wrapper."""
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
- emit({'task': 'land', 'ok': True, 'score': row.get('score', row.get('rating', row.get('theta', 0))), 'rating': row.get('rating', row.get('theta', 0)), 'wins': row.get('wins', 0), 'losses': row.get('losses', 0), 'draws': row.get('draws', 0)})
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)})