@hona/openeval 0.3.3 → 0.5.0

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 (204) hide show
  1. package/JUDGING.md +63 -1
  2. package/README.md +4 -0
  3. package/VERIFICATION.md +100 -0
  4. package/package.json +2 -2
  5. package/src/app/grade-recording.ts +16 -0
  6. package/src/app/input-fingerprints.ts +8 -2
  7. package/src/app/judge-evidence.ts +8 -2
  8. package/src/app/judge-run.ts +10 -0
  9. package/src/app/load-benchmark.ts +102 -9
  10. package/src/app/prepare-inputs.ts +40 -0
  11. package/src/app/read-results.ts +23 -5
  12. package/src/app/rejudge.ts +5 -0
  13. package/src/app/run-benchmark.ts +36 -4
  14. package/src/app/scores.ts +19 -2
  15. package/src/app/serve-results.ts +14 -0
  16. package/src/cli.ts +27 -2
  17. package/src/criterion-categories.ts +53 -0
  18. package/src/index.ts +13 -0
  19. package/src/infra/containers/catalog.ts +77 -21
  20. package/src/infra/containers/oci.ts +4 -0
  21. package/src/infra/judging/code-criteria.ts +112 -0
  22. package/src/infra/judging/code-result.ts +21 -3
  23. package/src/infra/judging/code-source.ts +10 -2
  24. package/src/infra/judging/code-worker.ts +5 -3
  25. package/src/infra/judging/code.ts +19 -3
  26. package/src/infra/judging/contract.ts +10 -2
  27. package/src/infra/judging/index.ts +1 -0
  28. package/src/infra/recording/index.ts +14 -2
  29. package/src/infra/verification/image.ts +33 -0
  30. package/src/infra/verification/runtime/Dockerfile +18 -0
  31. package/src/infra/verification/runtime/bun.lock +16 -0
  32. package/src/infra/verification/runtime/package.json +5 -0
  33. package/src/infra/verification/session.ts +199 -0
  34. package/src/judge-context.ts +66 -0
  35. package/src/types.ts +22 -2
  36. package/src/view.ts +59 -0
  37. package/viewer/assets/{abnfDiagram-VCTEODGH-Cl3HgjMn.js → abnfDiagram-VCTEODGH-tXaAmCdr.js} +1 -1
  38. package/viewer/assets/{angular-html-DD_Qs90j.js → angular-html-CNhJPCRr.js} +1 -1
  39. package/viewer/assets/{angular-ts-C6uwolJ0.js → angular-ts-BnGgGecY.js} +1 -1
  40. package/viewer/assets/{apl-DXXNG1AP.js → apl-ZOvmk1hE.js} +1 -1
  41. package/viewer/assets/{arc-VljIODVM.js → arc-BalFY-0o.js} +1 -1
  42. package/viewer/assets/architecture-7GRP2DOG-Dwn_SxkP.js +1 -0
  43. package/viewer/assets/{architectureDiagram-5GKGNRK7-BBPZQ1uU.js → architectureDiagram-5GKGNRK7-DpSDAEgb.js} +1 -1
  44. package/viewer/assets/{astro-DZiA762t.js → astro-BJzTFZ3R.js} +1 -1
  45. package/viewer/assets/{blade-0_CSK9rb.js → blade-CvS8_w1j.js} +1 -1
  46. package/viewer/assets/{blockDiagram-I7D4REHJ-C3jAen-I.js → blockDiagram-I7D4REHJ-BY3upSR4.js} +1 -1
  47. package/viewer/assets/{c-Dr4MdHFU.js → c-D5ZSBtUG.js} +1 -1
  48. package/viewer/assets/{c4Diagram-7LVT6UL2-x9-A2uwu.js → c4Diagram-7LVT6UL2-CA9keRc2.js} +1 -1
  49. package/viewer/assets/channel-CgM1_6P7.js +1 -0
  50. package/viewer/assets/{chapel-Bzsv5OV_.js → chapel-1aYDxJri.js} +1 -1
  51. package/viewer/assets/{chunk-4HAMMTFA-DhJ5vlqv.js → chunk-4HAMMTFA-B0uITxv4.js} +1 -1
  52. package/viewer/assets/{chunk-75Z2AOVW-BiWuU3A8.js → chunk-75Z2AOVW-CbTHx8Lz.js} +1 -1
  53. package/viewer/assets/{chunk-DU6HZSFF-DQsE2_KX.js → chunk-DU6HZSFF-C7mZpnch.js} +1 -1
  54. package/viewer/assets/{chunk-F27PBJKO-eYpvp3yv.js → chunk-F27PBJKO-iUZG9l9x.js} +1 -1
  55. package/viewer/assets/{chunk-GMAD6QVW-Cuv8q7cp.js → chunk-GMAD6QVW-BYg8qa71.js} +1 -1
  56. package/viewer/assets/{chunk-GVQU2GXP-C80TX_-9.js → chunk-GVQU2GXP-rnsdTI4Q.js} +1 -1
  57. package/viewer/assets/{chunk-IMKFNOWR-D6UIvU6O.js → chunk-IMKFNOWR-D0e2NIBw.js} +1 -1
  58. package/viewer/assets/{chunk-L3NEJ4N5-B97oN3Wz.js → chunk-L3NEJ4N5-Itlw7HBQ.js} +1 -1
  59. package/viewer/assets/{chunk-OSK3NFVY-Dv9V7Vwg.js → chunk-OSK3NFVY-DjAAQhPw.js} +1 -1
  60. package/viewer/assets/{chunk-P2QGCYS3-CCWmLNYJ.js → chunk-P2QGCYS3-XOxrUS-o.js} +1 -1
  61. package/viewer/assets/{chunk-POPQ4Y6H-B1p9ouPt.js → chunk-POPQ4Y6H-SHuwgciA.js} +1 -1
  62. package/viewer/assets/{chunk-PWAF6VOD-DA8KGNWy.js → chunk-PWAF6VOD-BtrRW-Sl.js} +1 -1
  63. package/viewer/assets/{chunk-SHT3W25Y-BrG5_y-x.js → chunk-SHT3W25Y-6Pacfssp.js} +1 -1
  64. package/viewer/assets/{chunk-SVP7TREG-BfR4k1NG.js → chunk-SVP7TREG-eTVS9a20.js} +1 -1
  65. package/viewer/assets/{chunk-TICWLB2K-Cl_FiqB8.js → chunk-TICWLB2K-CPnAfOZj.js} +1 -1
  66. package/viewer/assets/{chunk-XXDRQBXY-D4qiShQC.js → chunk-XXDRQBXY-ciXhM2Hb.js} +1 -1
  67. package/viewer/assets/classDiagram-ZZMXUADV-BtFc7wo5.js +1 -0
  68. package/viewer/assets/classDiagram-v2-VYDZK3BY-BtFc7wo5.js +1 -0
  69. package/viewer/assets/{cobol-BnQBc2SA.js → cobol-CEoq_5kf.js} +1 -1
  70. package/viewer/assets/{coffee-D8jvqPgZ.js → coffee-CZTWsTxw.js} +1 -1
  71. package/viewer/assets/{cose-bilkent-JH36ORCC-BcmCCZr6.js → cose-bilkent-JH36ORCC-NCkOcFkY.js} +1 -1
  72. package/viewer/assets/{cpp-MUcJROqQ.js → cpp-Hh-o0ivR.js} +1 -1
  73. package/viewer/assets/{crystal-CFGJH8of.js → crystal-CL_KTsac.js} +1 -1
  74. package/viewer/assets/{css-Z5Q-tnUN.js → css-9neLgVLd.js} +1 -1
  75. package/viewer/assets/{cynefin-OW5HDTMX-Hu_taNlH.js → cynefin-OW5HDTMX-Ba1tlgBB.js} +1 -1
  76. package/viewer/assets/{cynefinDiagram-5FMLGOSQ-DGCR5Bu-.js → cynefinDiagram-5FMLGOSQ-CMrzWo3p.js} +1 -1
  77. package/viewer/assets/{dagre-GXQ25YYZ-BONZK_72.js → dagre-GXQ25YYZ-DwjOnUDD.js} +1 -1
  78. package/viewer/assets/{diagram-S7CK7UJ4-B3PuH0gY.js → diagram-S7CK7UJ4-DA0PXsM6.js} +1 -1
  79. package/viewer/assets/{diagram-UQ7AKVKN-_nLXEPxU.js → diagram-UQ7AKVKN-C5BBFVvO.js} +1 -1
  80. package/viewer/assets/{diagram-VSXAHHWV-W8uCnJKR.js → diagram-VSXAHHWV-COgLeOyi.js} +1 -1
  81. package/viewer/assets/{diagram-VX7I27RA-Dw85ssfk.js → diagram-VX7I27RA-CEu-Pm3B.js} +1 -1
  82. package/viewer/assets/{diagram-Z3DM3KII-iT_nC6Ci.js → diagram-Z3DM3KII-BGq1_7BE.js} +1 -1
  83. package/viewer/assets/{dist-CQ0iZX7g.js → dist-CX8-lyNa.js} +1 -1
  84. package/viewer/assets/{ebnfDiagram-PWID7BFC-CmB3I8gj.js → ebnfDiagram-PWID7BFC-CGoEp9fJ.js} +1 -1
  85. package/viewer/assets/{edge-ChegMQD3.js → edge-CTz4kCIs.js} +1 -1
  86. package/viewer/assets/{elixir-BIS6GmoK.js → elixir-Bi8YL7XX.js} +1 -1
  87. package/viewer/assets/{elm-0b95WpPs.js → elm-C-UxMmSh.js} +1 -1
  88. package/viewer/assets/{erDiagram-RLTQ6QDP-BdrQ4dDR.js → erDiagram-RLTQ6QDP-Bb0ICmqZ.js} +1 -1
  89. package/viewer/assets/{erb-B6AHMmNJ.js → erb-ZSq62Y_u.js} +1 -1
  90. package/viewer/assets/eventmodeling-NTZA5JFV-BCWgnTvv.js +1 -0
  91. package/viewer/assets/flowDiagram-HODETNUW-B4hhZ1QN.js +1 -0
  92. package/viewer/assets/{ganttDiagram-EL5Y4UJY-CScNarbN.js → ganttDiagram-EL5Y4UJY-B4KN9ny5.js} +1 -1
  93. package/viewer/assets/{git-rebase-eLwqxn21.js → git-rebase-Cw9GoTpw.js} +1 -1
  94. package/viewer/assets/{gitGraph-4MIJSDKK-DBJydFU4.js → gitGraph-4MIJSDKK-CI3xWhLt.js} +1 -1
  95. package/viewer/assets/{gitGraphDiagram-WWUBYQGX-BNpJTag6.js → gitGraphDiagram-WWUBYQGX-CPv8-MbI.js} +1 -1
  96. package/viewer/assets/{glimmer-js-BPUd-Hdw.js → glimmer-js-Bb_1emHy.js} +1 -1
  97. package/viewer/assets/{glimmer-ts-BsY3Gaee.js → glimmer-ts-D2kOXMB0.js} +1 -1
  98. package/viewer/assets/{glsl-BgZlzkDK.js → glsl-DEqjHGFr.js} +1 -1
  99. package/viewer/assets/{graphql-BNNbyKev.js → graphql-Bi0OE4k4.js} +1 -1
  100. package/viewer/assets/{hack-DXqsPyfj.js → hack-aU4VRzRW.js} +1 -1
  101. package/viewer/assets/{haml-CEIe07b8.js → haml-B-2wwgyn.js} +1 -1
  102. package/viewer/assets/{handlebars-4TLrUOf9.js → handlebars-Bmq7hbFI.js} +1 -1
  103. package/viewer/assets/{html-Dirc4JcS.js → html-BBsNbpya.js} +1 -1
  104. package/viewer/assets/{html-derivative-lYLgmFdz.js → html-derivative-CkVRX-vn.js} +1 -1
  105. package/viewer/assets/{http-BhrBDOp-.js → http-cQ-nDL4L.js} +1 -1
  106. package/viewer/assets/{hurl-DWBDJqQR.js → hurl-D5phPwZN.js} +1 -1
  107. package/viewer/assets/{index-w8_YIakp.css → index-DdaXmlLv.css} +1 -1
  108. package/viewer/assets/index-Dhnl4xDm.js +795 -0
  109. package/viewer/assets/{info-A6RAGUB7-Cqp6XvkH.js → info-A6RAGUB7-CRDxOY87.js} +1 -1
  110. package/viewer/assets/{infoDiagram-27XIBGKW-CCvdaWwj.js → infoDiagram-27XIBGKW-D6y32CfO.js} +1 -1
  111. package/viewer/assets/{ishikawaDiagram-5VMMS53U-DQ4-h-VU.js → ishikawaDiagram-5VMMS53U-BWFBTCh5.js} +1 -1
  112. package/viewer/assets/{java-CdPCMwep.js → java-CT3DXSYV.js} +1 -1
  113. package/viewer/assets/{javascript-CvM774KP.js → javascript-BPmsAXaP.js} +1 -1
  114. package/viewer/assets/{jinja-BGKKdTiv.js → jinja-Ct_36dEc.js} +1 -1
  115. package/viewer/assets/{jison-CXj9ikNa.js → jison-BYrmy85c.js} +1 -1
  116. package/viewer/assets/{journeyDiagram-3NMN7TZE-DUzDc5Z8.js → journeyDiagram-3NMN7TZE-6XCObBEx.js} +1 -1
  117. package/viewer/assets/{json-NHDEs7sd.js → json-BCFP8udh.js} +1 -1
  118. package/viewer/assets/{jsx-FNvyvbBd.js → jsx-Dgzhn0Dq.js} +1 -1
  119. package/viewer/assets/{julia-B3kEyS8t.js → julia-DIddcjVY.js} +1 -1
  120. package/viewer/assets/{just-CpYQ-9KV.js → just-C5t963St.js} +1 -1
  121. package/viewer/assets/{kanban-definition-UXKFOSKX-ChHjSmhz.js → kanban-definition-UXKFOSKX-DU1B45HL.js} +1 -1
  122. package/viewer/assets/{latex-OWDJhn4l.js → latex-BKZooW5w.js} +1 -1
  123. package/viewer/assets/{line-DA6hWqrZ.js → line-lYgs1iW7.js} +1 -1
  124. package/viewer/assets/{linear-UNQGDr0C.js → linear-sHqbNFGm.js} +1 -1
  125. package/viewer/assets/{liquid-PySKINiF.js → liquid-_PLVu-8C.js} +1 -1
  126. package/viewer/assets/{lua-DVWbE3Qa.js → lua-BWNAOGrF.js} +1 -1
  127. package/viewer/assets/{marko-9EBuCkFn.js → marko-DC7qz2de.js} +1 -1
  128. package/viewer/assets/{mdc-BlHUSA6C.js → mdc-Bn_2wB99.js} +1 -1
  129. package/viewer/assets/{mermaid-parser.core-86Qkc0ID.js → mermaid-parser.core-jzj7JZTj.js} +3 -3
  130. package/viewer/assets/{mermaid.core-DaVdMmkQ.js → mermaid.core-DpRyWAPv.js} +4 -4
  131. package/viewer/assets/{mindmap-definition-YA3MSWOX-oloAEnnx.js → mindmap-definition-YA3MSWOX-CIZjqKbE.js} +1 -1
  132. package/viewer/assets/{nginx-CDJjPEvc.js → nginx-Ce7JLwNg.js} +1 -1
  133. package/viewer/assets/{nim-DNAK-c_E.js → nim-CP_MVph6.js} +1 -1
  134. package/viewer/assets/{org-D6PZe7Hw.js → org-BVKft-Jg.js} +1 -1
  135. package/viewer/assets/{packet-AYTQ26CC-CF1AN50P.js → packet-AYTQ26CC-BwRTCCV1.js} +1 -1
  136. package/viewer/assets/{pegDiagram-XKGWAZYB-DGz2-yRO.js → pegDiagram-XKGWAZYB-3nilNfrm.js} +1 -1
  137. package/viewer/assets/{perl-DIspB-Mg.js → perl-DRhmHTw_.js} +1 -1
  138. package/viewer/assets/{php-BVepTqtk.js → php-CrnivzIK.js} +1 -1
  139. package/viewer/assets/{pie-WAS4IAKB-Cx1FR1mr.js → pie-WAS4IAKB-Dn0ztV-V.js} +1 -1
  140. package/viewer/assets/{pieDiagram-E7YTZNPT-BbbWhyZI.js → pieDiagram-E7YTZNPT-BFj69QcO.js} +1 -1
  141. package/viewer/assets/{pug-DAPcGjX6.js → pug-BfP-YwOD.js} +1 -1
  142. package/viewer/assets/{qml-DWWtxGbA.js → qml-BHf9C17M.js} +1 -1
  143. package/viewer/assets/{quadrantDiagram-AXDQQJYC-DrUJjgA1.js → quadrantDiagram-AXDQQJYC-2t5I-_4_.js} +1 -1
  144. package/viewer/assets/{r-Mpv0S460.js → r-DjPtVJdp.js} +1 -1
  145. package/viewer/assets/{radar-RG4KPBEZ-y9SAfPV_.js → radar-RG4KPBEZ-D14Z-9c0.js} +1 -1
  146. package/viewer/assets/{railroad-74A4TZTK-Dfc0Q6_w.js → railroad-74A4TZTK-BuxyK73P.js} +1 -1
  147. package/viewer/assets/railroad-abnf-HS5TGJTU-BM7gtNXd.js +1 -0
  148. package/viewer/assets/railroad-ebnf-LZEXJU2U-DPlBcoln.js +1 -0
  149. package/viewer/assets/railroad-peg-WCYAUIDC-B6tOGY79.js +1 -0
  150. package/viewer/assets/{railroadDiagram-O6MQD6OU-Koe-8k88.js → railroadDiagram-O6MQD6OU-Dm2mv6N0.js} +1 -1
  151. package/viewer/assets/{razor-1HRaok3B.js → razor-BH3XX51e.js} +1 -1
  152. package/viewer/assets/{regexp-dtCIuY15.js → regexp-DPzTmgy2.js} +1 -1
  153. package/viewer/assets/{requirementDiagram-BXWQKSXE-BkisH0SS.js → requirementDiagram-BXWQKSXE-BJTZ8ml9.js} +1 -1
  154. package/viewer/assets/{rst-DHb1VRTR.js → rst-DQbfRM9X.js} +1 -1
  155. package/viewer/assets/{ruby-BFIUXRbM.js → ruby-DxybHg9D.js} +1 -1
  156. package/viewer/assets/{sankeyDiagram-P5KCCOFB-B7LDcgGb.js → sankeyDiagram-P5KCCOFB-eBLIKN_A.js} +1 -1
  157. package/viewer/assets/{sas-CXVQStV2.js → sas-DoUzRWOs.js} +1 -1
  158. package/viewer/assets/{scss-D1TWv7v-.js → scss-BSqQF9Aq.js} +1 -1
  159. package/viewer/assets/{sequenceDiagram-WJ2MYXX4-CVdRHXxu.js → sequenceDiagram-WJ2MYXX4-BxHxbxqV.js} +1 -1
  160. package/viewer/assets/{shellscript-BNxbdlcG.js → shellscript-DkZJ1HjW.js} +1 -1
  161. package/viewer/assets/{shellsession-BPgWxeMC.js → shellsession-ykUTtcwM.js} +1 -1
  162. package/viewer/assets/{soy-xvxZ4puZ.js → soy-4rmF0gor.js} +1 -1
  163. package/viewer/assets/{sql-mcloyhsl.js → sql-wJX-vNm_.js} +1 -1
  164. package/viewer/assets/{src-CEugxhjf.js → src-IhQtBUXO.js} +1 -1
  165. package/viewer/assets/{stata-BH9WxX3M.js → stata-Bjw5vh7C.js} +1 -1
  166. package/viewer/assets/{stateDiagram-D77RDMKH-DCe7c0bj.js → stateDiagram-D77RDMKH-DNPBpLlw.js} +1 -1
  167. package/viewer/assets/stateDiagram-v2-MP3YSRHH-sLT-i9tN.js +1 -0
  168. package/viewer/assets/{surrealql-DT6z9HjU.js → surrealql-B-cZs35d.js} +1 -1
  169. package/viewer/assets/{svelte-DhMjoL5f.js → svelte-BtYh0aTf.js} +1 -1
  170. package/viewer/assets/{swimlanes-42K2YHIH-0HmmIS3k.js → swimlanes-42K2YHIH-CnHbZ76W.js} +1 -1
  171. package/viewer/assets/swimlanesDiagram-VR7AAH4N-DIpLefsN.js +8 -0
  172. package/viewer/assets/{templ-DommSy4z.js → templ-eYWdvrUr.js} +1 -1
  173. package/viewer/assets/{tex-CaFWJFN-.js → tex-DQ-rt_Oj.js} +1 -1
  174. package/viewer/assets/{timeline-definition-24CTP7MA-od0g4NnC.js → timeline-definition-24CTP7MA-vOTFZgf6.js} +1 -1
  175. package/viewer/assets/{treeView-Q6P3EWNA-BI6VSGO-.js → treeView-Q6P3EWNA-CU_mSunf.js} +1 -1
  176. package/viewer/assets/{treemap-WGGIJYW6-Bk8N6Y68.js → treemap-WGGIJYW6-BbLri-XL.js} +1 -1
  177. package/viewer/assets/{ts-tags-BYVx2WcX.js → ts-tags-CspMWJ3s.js} +1 -1
  178. package/viewer/assets/{tsx-nHQYkkGG.js → tsx-CXUmELmz.js} +1 -1
  179. package/viewer/assets/{twig-Cw-lWIlb.js → twig-B0yioFX1.js} +1 -1
  180. package/viewer/assets/{typescript-DygBWTVv.js → typescript-vMN-etbi.js} +1 -1
  181. package/viewer/assets/{typst-CGHo10wY.js → typst-Cvk0bEbK.js} +1 -1
  182. package/viewer/assets/{vennDiagram-4TSXK5OY-DH3Uf812.js → vennDiagram-4TSXK5OY-PBkXHE6T.js} +1 -1
  183. package/viewer/assets/{vue-CX1DrRJy.js → vue-Ce21QNcM.js} +1 -1
  184. package/viewer/assets/{vue-html-c2PT7ajU.js → vue-html-DsNf3O2s.js} +1 -1
  185. package/viewer/assets/{vue-vine-R8BVuMuN.js → vue-vine-BsGtGXV3.js} +1 -1
  186. package/viewer/assets/{wardley-WFR3VGLG-DaBE6t7F.js → wardley-WFR3VGLG-kwPRtpGA.js} +1 -1
  187. package/viewer/assets/{wardleyDiagram-VM6X3IG4-CpuxvD_D.js → wardleyDiagram-VM6X3IG4-BacMGxmX.js} +1 -1
  188. package/viewer/assets/{xml-CtozFVgZ.js → xml-BmjAaPhA.js} +1 -1
  189. package/viewer/assets/{xsl-Bywa5lsK.js → xsl-CtfA1v1E.js} +1 -1
  190. package/viewer/assets/{xychartDiagram-S5SC5T6Z-CIooxwr4.js → xychartDiagram-S5SC5T6Z-BDIbR1Lm.js} +1 -1
  191. package/viewer/assets/{yaml-m1ZbeIKu.js → yaml-A9EbijaD.js} +1 -1
  192. package/viewer/index.html +2 -2
  193. package/viewer/assets/architecture-7GRP2DOG-cmvaWKJl.js +0 -1
  194. package/viewer/assets/channel-BrXVTGcv.js +0 -1
  195. package/viewer/assets/classDiagram-ZZMXUADV-BsgTq0ME.js +0 -1
  196. package/viewer/assets/classDiagram-v2-VYDZK3BY-BsgTq0ME.js +0 -1
  197. package/viewer/assets/eventmodeling-NTZA5JFV-D1Zwl9Cr.js +0 -1
  198. package/viewer/assets/flowDiagram-HODETNUW-Dfw-HgvJ.js +0 -1
  199. package/viewer/assets/index-LKycHUia.js +0 -795
  200. package/viewer/assets/railroad-abnf-HS5TGJTU-CB_PJcK6.js +0 -1
  201. package/viewer/assets/railroad-ebnf-LZEXJU2U-Bwgt_HNz.js +0 -1
  202. package/viewer/assets/railroad-peg-WCYAUIDC-DkZKEvIQ.js +0 -1
  203. package/viewer/assets/stateDiagram-v2-MP3YSRHH-DAiJpzC0.js +0 -1
  204. package/viewer/assets/swimlanesDiagram-VR7AAH4N-rLdQ1dbD.js +0 -8
@@ -1,5 +1,5 @@
1
- import { readdir } from "node:fs/promises";
2
- import { resolve, dirname } from "node:path";
1
+ import { readdir, realpath } from "node:fs/promises";
2
+ import { resolve, dirname, sep } from "node:path";
3
3
  import { Results } from "../infra/sqlite";
4
4
  import type { BenchmarkRun, EvalRun, JudgeRun, Slot, Cost } from "../types";
5
5
  import type {
@@ -14,7 +14,7 @@ import type {
14
14
  StageStatus,
15
15
  JudgeAudit,
16
16
  } from "../view";
17
- import { sumCosts } from "../view";
17
+ import { categoryCatalog, sumCosts } from "../view";
18
18
  import { benchmarkScores } from "./scores";
19
19
  import { executionRuntime } from "./execution-runtime";
20
20
  import {
@@ -26,7 +26,7 @@ import {
26
26
  } from "../runtime";
27
27
  import { canJudgeEval } from "./eval-state";
28
28
  import { CandidateEvidence, type EvidenceQuery } from "../infra/evidence";
29
- import { contained } from "../infra/files";
29
+ import { contained, hash } from "../infra/files";
30
30
 
31
31
  const scheduled = (slot: Slot, benchmark: BenchmarkRun, now: number) =>
32
32
  benchmark.state === "running" &&
@@ -167,7 +167,7 @@ export class ResultReader {
167
167
  if (!judge) throw new Error("Unknown judge run");
168
168
  const candidate = results.evalRun(judge.input.evalRunId);
169
169
  const criteria = new Map(
170
- judge.input.criteria.map((criterion) => [criterion.id, criterion]),
170
+ [...judge.input.criteria, ...judge.input.codeCriteria ?? []].map((criterion) => [criterion.id, criterion]),
171
171
  );
172
172
  for (const id of Object.keys(judge.judgment?.scores ?? {}))
173
173
  if (!criteria.has(id))
@@ -225,6 +225,23 @@ export class ResultReader {
225
225
  })),
226
226
  };
227
227
  }
228
+ async verificationEvidence(benchmarkId: string, judgeRunId: string, id: string, path?: string) {
229
+ using results = await this.database(benchmarkId);
230
+ const judge = results.judgeRun(judgeRunId);
231
+ const verification = judge?.code?.verifications?.find(item => item.id === id);
232
+ if (!verification) throw new Error("Unknown verification");
233
+ if (!path) return { receipt: verification };
234
+ const artifact = verification.artifacts.find(item => item.path === path);
235
+ const file = path === "stdout" ? verification.stdout : path === "stderr" ? verification.stderr : artifact?.file;
236
+ if (!file) throw new Error("Unknown verification artifact");
237
+ const root = await realpath(dirname(results.path));
238
+ const target = await realpath(contained(root, file));
239
+ if (target !== root && !target.startsWith(root + sep)) throw new Error("Verification artifact leaves its run directory");
240
+ const bytes = await Bun.file(target).bytes();
241
+ if (bytes.length > 32 * 1024 * 1024 || (artifact && hash(bytes) !== artifact.sha256))
242
+ throw new Error("Verification artifact is too large or its hash differs");
243
+ return { bytes, path };
244
+ }
228
245
  async checkEvidence(
229
246
  benchmarkId: string,
230
247
  judgeRunId: string,
@@ -316,6 +333,7 @@ export class ResultReader {
316
333
  evals.map((item) => [item.id, item.name]),
317
334
  ),
318
335
  modelNames: run.modelNames ?? {},
336
+ categories: categoryCatalog(scores[0]?.components ?? []),
319
337
  cost: evalId ? evalCosts[evalId] : sumCosts(Object.values(evalCosts)),
320
338
  evalCosts,
321
339
  runtime,
@@ -7,6 +7,7 @@ import { judgeEvalRun } from "./judge-run";
7
7
  import { executeQueue, finishBenchmark } from "./run-benchmark";
8
8
  import { judgingFingerprint } from "../infra/judging";
9
9
  import { canJudgeEval } from "./eval-state";
10
+ import { verificationRuntime } from "../infra/verification/image";
10
11
 
11
12
  /** Rejudging changes only the selected judgment. The candidate execution is never repeated. */
12
13
  export async function judgeRun(
@@ -59,6 +60,8 @@ export async function judgeRuns(
59
60
  judgeHash: updated.judgeHash,
60
61
  criteria: updated.criteria,
61
62
  code: updated.code,
63
+ codeCriteria: updated.codeCriteria,
64
+ categories: updated.categories,
62
65
  name: updated.name,
63
66
  }
64
67
  : evalDefinition;
@@ -67,7 +70,9 @@ export async function judgeRuns(
67
70
  const runtime = {
68
71
  ...previous.runtime,
69
72
  judgeHash: await judgingFingerprint(),
73
+ verification: await verificationRuntime(definition.judge.verification),
70
74
  };
75
+ if (runtime.verification) definition.judge.verification = runtime.verification;
71
76
  const selected = new Set(candidates.map((candidate) => candidate.slotId));
72
77
  results.transaction(() => {
73
78
  if (results.benchmark?.state === "running")
@@ -23,6 +23,11 @@ import { canJudgeEval } from "./eval-state";
23
23
  import { isScored } from "../judgment";
24
24
  import { benchmarkScores } from "./scores";
25
25
  import { CostBudget, estimateWork } from "./cost-plan";
26
+ import {
27
+ categoryKey,
28
+ categoryList,
29
+ inCategories,
30
+ } from "../criterion-categories";
26
31
 
27
32
  export type RunBenchmarkOptions = {
28
33
  directory?: string;
@@ -32,6 +37,8 @@ export type RunBenchmarkOptions = {
32
37
  /** Execute work only for these models while retaining the full aggregate. */
33
38
  onlyModels?: readonly ModelRef[];
34
39
  onlyEvals?: readonly string[];
40
+ /** Execute only evals with a criterion in any of these categories. */
41
+ onlyCategories?: readonly string[];
35
42
  onlyRepetitions?: readonly number[];
36
43
  /** Stop admitting work when reported spend plus reservations reaches this amount. */
37
44
  maxCostUSD?: number;
@@ -39,6 +46,29 @@ export type RunBenchmarkOptions = {
39
46
  onEvent?: ExecutionObserver;
40
47
  };
41
48
 
49
+ /** Intersect --only-eval with evals that have a criterion in any --only-category. */
50
+ function scopedEvals(
51
+ definition: BenchmarkDefinition,
52
+ options: Pick<RunBenchmarkOptions, "onlyEvals" | "onlyCategories">,
53
+ ) {
54
+ if (!options.onlyCategories) return options.onlyEvals;
55
+ const keys = categoryList(options.onlyCategories, "--only-category").map(
56
+ categoryKey,
57
+ );
58
+ if (!keys.length) throw new Error("Select at least one category");
59
+ const matching = definition.evals
60
+ .filter((item) =>
61
+ Object.values(item.categories ?? {}).some((names) =>
62
+ inCategories({ categories: names }, keys),
63
+ ),
64
+ )
65
+ .map((item) => item.id)
66
+ .filter((id) => !options.onlyEvals || options.onlyEvals.includes(id));
67
+ if (!matching.length)
68
+ throw new Error("No selected evals have criteria in these categories");
69
+ return matching;
70
+ }
71
+
42
72
  export async function currentBenchmarkRun(directory: string) {
43
73
  const root = resolve(directory, "results");
44
74
  const dirs = (await readdir(root, { withFileTypes: true }).catch(() => []))
@@ -141,11 +171,13 @@ export async function runBenchmark(
141
171
  ))
142
172
  )
143
173
  throw new Error("Select configured eval IDs with --only-eval");
174
+ const onlyEvals = scopedEvals(definition, options);
144
175
  new CostBudget(options.maxCostUSD, () => 0);
145
176
  const selected = options.onlyModels?.map(modelRef);
146
177
  if (selected && !selected.length)
147
178
  throw new Error("Select at least one model");
148
- const runtime = await runtimeFingerprint(definition.container);
179
+ const runtime = await runtimeFingerprint(definition.container, definition.judge.verification);
180
+ if (runtime.verification) definition.judge.verification = runtime.verification;
149
181
  let directory =
150
182
  options.directory ??
151
183
  (!options.fresh
@@ -192,11 +224,11 @@ export async function runBenchmark(
192
224
  results.slots().map((slot) => [slot.id, slot]),
193
225
  );
194
226
  let plan = planBenchmark(definition, runtime, results);
195
- if (selected || options.onlyEvals || options.onlyRepetitions) {
227
+ if (selected || onlyEvals || options.onlyRepetitions) {
196
228
  const retained = new Map(results.slots().map((slot) => [slot.id, slot]));
197
229
  plan = plan.map((item) =>
198
230
  ((!selected || selected.includes(item.slot.model)) &&
199
- (!options.onlyEvals || options.onlyEvals.includes(item.slot.evalId)) &&
231
+ (!onlyEvals || onlyEvals.includes(item.slot.evalId)) &&
200
232
  (!options.onlyRepetitions ||
201
233
  options.onlyRepetitions.includes(item.slot.repetition))) ||
202
234
  item.action === "reuse"
@@ -244,7 +276,7 @@ export async function runBenchmark(
244
276
  execution: {
245
277
  startedAt: now,
246
278
  onlyModels: selected,
247
- onlyEvals: options.onlyEvals,
279
+ onlyEvals,
248
280
  onlyRepetitions: options.onlyRepetitions,
249
281
  budgetUSD: options.maxCostUSD,
250
282
  estimatedUSD: estimate.estimatedUSD,
package/src/app/scores.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { BenchmarkDefinition, JudgeRun, Slot } from "../types";
2
2
  import { modelScore } from "../view";
3
3
  import { isScored } from "../judgment";
4
+ import { categoryKey, inCategories } from "../criterion-categories";
4
5
 
5
6
  /** Scores are derived from a single selection snapshot; every eval has equal weight. */
6
7
  export function benchmarkScores(
@@ -13,10 +14,14 @@ export function benchmarkScores(
13
14
  const evals = definition.evals.filter(
14
15
  (item) => !evalId || item.id === evalId,
15
16
  );
17
+ const keys = definition.categories?.map(categoryKey);
16
18
  const definitions = new Map(
17
19
  evals.map((item) => {
18
20
  const criteria = new Map(
19
- item.criteria.map((criterion) => [criterion.id, criterion]),
21
+ [...item.criteria, ...(item.codeCriteria ?? [])].map((criterion) => [
22
+ criterion.id,
23
+ criterion,
24
+ ]),
20
25
  );
21
26
  for (const slot of slots.filter(
22
27
  (slot) => slot.active && slot.evalId === item.id,
@@ -28,7 +33,16 @@ export function benchmarkScores(
28
33
  if (!criteria.has(id))
29
34
  criteria.set(id, { id, name: id.replaceAll("_", " ") });
30
35
  }
31
- return [item.id, [...criteria.values()]];
36
+ const categorized = [...criteria.values()].map((criterion) => ({
37
+ ...criterion,
38
+ categories: item.categories?.[criterion.id] ?? [],
39
+ }));
40
+ return [
41
+ item.id,
42
+ keys
43
+ ? categorized.filter((criterion) => inCategories(criterion, keys))
44
+ : categorized,
45
+ ];
32
46
  }),
33
47
  );
34
48
  return definition.models.map((model) =>
@@ -57,6 +71,9 @@ export function benchmarkScores(
57
71
  eval: item.id,
58
72
  criterion: criterion.id,
59
73
  name: criterion.name,
74
+ ...(criterion.categories.length
75
+ ? { categories: criterion.categories }
76
+ : {}),
60
77
  value:
61
78
  values.length === definition.repetitions
62
79
  ? scoredSum / definition.repetitions
@@ -52,6 +52,20 @@ export async function serveResults(options: {
52
52
  url.searchParams.get("judge") ?? "",
53
53
  ),
54
54
  );
55
+ if (url.pathname === "/api/verification") {
56
+ const result = await reader.verificationEvidence(
57
+ url.searchParams.get("benchmark") ?? "", url.searchParams.get("judge") ?? "",
58
+ url.searchParams.get("id") ?? "", url.searchParams.get("path") ?? undefined,
59
+ );
60
+ if ("receipt" in result) return Response.json(result);
61
+ const mime = /\.png$/i.test(result.path) ? "image/png" : /\.jpe?g$/i.test(result.path) ? "image/jpeg" :
62
+ /\.webp$/i.test(result.path) ? "image/webp" : "text/plain; charset=utf-8";
63
+ return new Response(result.bytes, { headers: {
64
+ "Content-Type": mime, "X-Content-Type-Options": "nosniff",
65
+ "Content-Security-Policy": "default-src 'none'; sandbox",
66
+ "Cache-Control": "no-store",
67
+ } });
68
+ }
55
69
  if (url.pathname === "/api/check-evidence") {
56
70
  const query: EvidenceQuery = {
57
71
  action: (url.searchParams.get("action") ??
package/src/cli.ts CHANGED
@@ -12,6 +12,9 @@ import {
12
12
  serveResults,
13
13
  mergeBenchmarkRuns,
14
14
  snapshotBenchmarkRun,
15
+ buildVerificationImage,
16
+ VERIFICATION_IMAGE,
17
+ prepareInputs,
15
18
  } from "./index";
16
19
  import type { ModelRef } from "./index";
17
20
 
@@ -35,6 +38,7 @@ try {
35
38
  Commands:
36
39
  image Build the candidate container image
37
40
  plan Show missing or changed work without executing it
41
+ prepare Prepare and archive inputs without model calls
38
42
  run Execute missing or changed work in the current result
39
43
  view Serve the results viewer
40
44
  snapshot <run> <name> Export a score snapshot
@@ -51,17 +55,33 @@ Options:
51
55
  --model <provider/id> Model to add or remove (repeatable)
52
56
  --only-model <ref> Execute only this model (repeatable)
53
57
  --only-eval <id> Execute only this eval (repeatable)
58
+ --only-category <name> Execute only evals with criteria in this category (repeatable)
54
59
  --only-repetition <n> Execute only this repetition (repeatable)
55
60
  --max-cost <usd> Scheduling budget for this invocation
56
61
  --final-only Judge only after candidates finish
62
+ --verification Build only the standard verification image (image command)
63
+ --output <dir> New prepared-input directory (prepare command)
57
64
  --port <port> Viewer port (default: 4173)
58
65
 
59
66
  Requires Bun 1.4.2+, Docker, and an authenticated OpenCode installation.`);
60
67
  } else if (command === "--version") {
61
68
  console.log((await Bun.file(new URL("../package.json", import.meta.url)).json()).version);
62
69
  } else if (command === "image") {
63
- await buildImage((await loadBenchmark(benchmark)).container);
64
- console.log("Candidate image ready");
70
+ const definition = await loadBenchmark(benchmark);
71
+ if (!args.includes("--verification")) {
72
+ await buildImage(definition.container);
73
+ console.log("Candidate image ready");
74
+ }
75
+ const verification = definition.judge.verification ?? (args.includes("--verification")
76
+ ? { image: VERIFICATION_IMAGE, engine: definition.container.engine } : undefined);
77
+ if (verification) {
78
+ await buildVerificationImage(verification);
79
+ console.log("Verification image ready");
80
+ }
81
+ } else if (command === "prepare") {
82
+ if (!option("--output")) throw new Error("prepare requires --output with a new directory");
83
+ const onlyEvals = args.flatMap((arg, index) => arg === "--only-eval" ? [args[index + 1]] : []);
84
+ console.log(JSON.stringify(await prepareInputs(benchmark, { directory: option("--output")!, onlyEvals: onlyEvals.length ? onlyEvals : undefined }), null, 2));
65
85
  } else if (command === "view") {
66
86
  const viewer = await serveResults({
67
87
  resultsPath: resolve(benchmark, "results"),
@@ -84,6 +104,11 @@ Requires Bun 1.4.2+, Docker, and an authenticated OpenCode installation.`);
84
104
  arg === "--only-eval" ? [args[index + 1]] : [],
85
105
  )
86
106
  : undefined,
107
+ onlyCategories: args.includes("--only-category")
108
+ ? args.flatMap((arg, index) =>
109
+ arg === "--only-category" ? [args[index + 1]] : [],
110
+ )
111
+ : undefined,
87
112
  maxCostUSD: args.includes("--max-cost")
88
113
  ? Number(option("--max-cost"))
89
114
  : undefined,
@@ -0,0 +1,53 @@
1
+ /** Categories are reporting metadata: any non-empty string, matched case-insensitively. */
2
+ export const categoryKey = (name: string) => name.trim().toLowerCase();
3
+
4
+ /** Trim names and drop case-insensitive duplicates, keeping the first spelling. */
5
+ export function categoryList(values: readonly unknown[], where: string) {
6
+ const names: string[] = [],
7
+ keys = new Set<string>();
8
+ for (const value of values) {
9
+ if (typeof value !== "string" || !value.trim())
10
+ throw new Error(`${where}: categories must be non-empty strings`);
11
+ const name = value.trim();
12
+ if (keys.has(categoryKey(name))) continue;
13
+ keys.add(categoryKey(name));
14
+ names.push(name);
15
+ }
16
+ return names;
17
+ }
18
+
19
+ const CATEGORY_LINE = /^Categor(?:y|ies):[ \t]*(.*?)\r?$/i;
20
+ const HEADING = /^## Criterion: ([a-z][a-z0-9_]*)[ \t]*[—–-]/;
21
+
22
+ /** Reads the optional `Categories:` line directly below each `## Criterion:` heading.
23
+ * The returned rubric omits those lines, so adding or changing categories never
24
+ * changes judge input or judge fingerprints.
25
+ */
26
+ export function rubricCategories(rubric: string) {
27
+ const lines = rubric.split("\n"),
28
+ kept: string[] = [],
29
+ categories: Record<string, string[]> = {};
30
+ for (let index = 0; index < lines.length; index++) {
31
+ const line = lines[index]!;
32
+ if (CATEGORY_LINE.test(line))
33
+ throw new Error(
34
+ "Put a Categories: line directly below its ## Criterion: heading",
35
+ );
36
+ kept.push(line);
37
+ const heading = HEADING.exec(line),
38
+ next = heading ? CATEGORY_LINE.exec(lines[index + 1] ?? "") : null;
39
+ if (!heading || !next) continue;
40
+ categories[heading[1]!] = categoryList(
41
+ next[1]!.split(","),
42
+ `Criterion ${heading[1]}`,
43
+ );
44
+ index++;
45
+ }
46
+ return { rubric: kept.join("\n"), categories };
47
+ }
48
+
49
+ /** Whether a scored criterion belongs to any of the selected category keys. */
50
+ export const inCategories = (
51
+ part: { categories?: readonly string[] },
52
+ keys: readonly string[],
53
+ ) => !!part.categories?.some((name) => keys.includes(categoryKey(name)));
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
  JudgeRun,
10
10
  Judgment,
11
11
  CriterionDefinition,
12
+ CodeCriteria,
12
13
  CriterionScore,
13
14
  EvidenceCitation,
14
15
  ToolCall,
@@ -31,6 +32,10 @@ export type {
31
32
  } from "./evidence";
32
33
  export { CANDIDATE_TIMEOUT_MS } from "./types";
33
34
  export { rubricCriteria, criterionMean, isScored } from "./judgment";
35
+ export {
36
+ categoryKey,
37
+ rubricCategories,
38
+ } from "./criterion-categories";
34
39
  export type {
35
40
  JudgeContext,
36
41
  JudgeFunction,
@@ -41,8 +46,16 @@ export type {
41
46
  RecordedEvent,
42
47
  RecordedMessage,
43
48
  RecordedFile,
49
+ CodeScore,
50
+ VerificationEnvironment,
51
+ VerificationRuntime,
52
+ VerificationRequest,
53
+ VerificationResult,
54
+ VerificationArtifact,
44
55
  } from "./judge-context";
56
+ export { buildVerificationImage, VERIFICATION_IMAGE } from "./infra/verification/image";
45
57
  export { loadBenchmark } from "./app/load-benchmark";
58
+ export { prepareInputs } from "./app/prepare-inputs";
46
59
  export {
47
60
  runBenchmark,
48
61
  currentBenchmarkRun,
@@ -1,18 +1,64 @@
1
1
  import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { resolve } from "node:path";
4
- import type { BenchmarkDefinition, ModelNames } from "../../types";
4
+ import type {
5
+ BenchmarkDefinition,
6
+ ModelNames,
7
+ ModelRef,
8
+ ProviderDefinitions,
9
+ } from "../../types";
5
10
  import { CandidateContainer } from "./oci";
6
11
  import { createSessionDatabase } from "../opencode/host";
7
12
  import { createCredentialSnapshot, integrationFor } from "../opencode/auth";
8
13
  import { clientFor, parseModel } from "../opencode/session";
14
+ import { candidateProviders } from "../../app/input-fingerprints";
9
15
 
10
16
  const CATALOG_TIMEOUT_MS = 60_000;
11
17
 
12
- /** Display names from the candidates' OpenCode catalog, including declared provider overrides. */
18
+ type CatalogDefinition = Pick<
19
+ BenchmarkDefinition,
20
+ "models" | "candidate" | "container"
21
+ >;
22
+
23
+ /** Display names from the candidates' OpenCode catalog, including declared provider overrides.
24
+ * Like candidates, each container receives only one integration's credentials and its models' provider configuration.
25
+ */
13
26
  export async function readModelNames(
14
- definition: Pick<BenchmarkDefinition, "models" | "candidate" | "container">,
27
+ definition: CatalogDefinition,
28
+ imageId: string,
29
+ ): Promise<ModelNames> {
30
+ const names = await Promise.all(
31
+ catalogRequests(definition).map((request) =>
32
+ readIntegrationNames(definition.container, imageId, request).catch(
33
+ () => ({}),
34
+ ),
35
+ ),
36
+ );
37
+ return Object.assign({}, ...names);
38
+ }
39
+
40
+ export type CatalogRequest = {
41
+ integration: string;
42
+ models: readonly ModelRef[];
43
+ providers?: ProviderDefinitions;
44
+ };
45
+
46
+ /** One request per credential integration, scoped like the candidates it names. */
47
+ export const catalogRequests = (
48
+ definition: Pick<BenchmarkDefinition, "models" | "candidate">,
49
+ ): CatalogRequest[] =>
50
+ [...Map.groupBy(definition.models, integrationFor)].map(
51
+ ([integration, models]) => ({
52
+ integration,
53
+ models,
54
+ providers: scopedProviders(definition.candidate.providers, models),
55
+ }),
56
+ );
57
+
58
+ async function readIntegrationNames(
59
+ container: BenchmarkDefinition["container"],
15
60
  imageId: string,
61
+ { integration, models, providers }: CatalogRequest,
16
62
  ): Promise<ModelNames> {
17
63
  const staging = await mkdtemp(
18
64
  resolve(process.env.TMP ?? tmpdir(), "catalog-"),
@@ -23,31 +69,19 @@ export async function readModelNames(
23
69
  await mkdir(workspace);
24
70
  await createSessionDatabase(
25
71
  database,
26
- createCredentialSnapshot(undefined, [
27
- ...new Set(definition.models.map(integrationFor)),
28
- ]),
29
- );
30
- await using container = await CandidateContainer.create(
31
- definition.container,
32
- imageId,
33
- );
34
- await container.prepare(
35
- workspace,
36
- database,
37
- false,
38
- [],
39
- staging,
40
- definition.candidate.providers,
72
+ createCredentialSnapshot(undefined, [integration]),
41
73
  );
74
+ await using catalog = await CandidateContainer.create(container, imageId);
75
+ await catalog.prepare(workspace, database, false, [], staging, providers);
42
76
  const client = clientFor(
43
- await container.start(CATALOG_TIMEOUT_MS),
44
- container.password,
77
+ await catalog.start(CATALOG_TIMEOUT_MS),
78
+ catalog.password,
45
79
  );
46
80
  const location = { directory: "/workspace" };
47
81
  await client.plugin.awaitActivation({ location });
48
82
  const listed = (await client.model.list({ location })).data;
49
83
  return Object.fromEntries(
50
- definition.models.flatMap((model) => {
84
+ models.flatMap((model) => {
51
85
  const selected = parseModel(model),
52
86
  found = listed.find(
53
87
  (item) =>
@@ -68,3 +102,25 @@ export async function readModelNames(
68
102
  });
69
103
  }
70
104
  }
105
+
106
+ /** Union of what each listed candidate would receive, without other providers' entries. */
107
+ function scopedProviders(
108
+ providers: ProviderDefinitions | undefined,
109
+ models: readonly ModelRef[],
110
+ ): ProviderDefinitions | undefined {
111
+ let scoped: NonNullable<ProviderDefinitions> | undefined;
112
+ for (const model of models)
113
+ for (const [id, entry] of Object.entries(
114
+ candidateProviders(providers, model) ?? {},
115
+ )) {
116
+ scoped ??= {};
117
+ scoped[id] = {
118
+ ...scoped[id],
119
+ ...entry,
120
+ ...(scoped[id]?.models || entry.models
121
+ ? { models: { ...scoped[id]?.models, ...entry.models } }
122
+ : {}),
123
+ };
124
+ }
125
+ return scoped;
126
+ }
@@ -12,6 +12,7 @@ import { extractWorkspaceArchive } from "./transfer";
12
12
  import { treeHash, fingerprint, writeJson } from "../files";
13
13
  import { OPENCODE_VERSION } from "../opencode/host";
14
14
  import { judgingFingerprint } from "../judging";
15
+ import { verificationRuntime } from "../verification/image";
15
16
 
16
17
  const runtimeDirectory = fileURLToPath(new URL("./runtime/", import.meta.url));
17
18
  export async function buildImage(config: BenchmarkDefinition["container"]) {
@@ -46,8 +47,10 @@ export async function inspectImage(config: BenchmarkDefinition["container"]) {
46
47
  }
47
48
  export async function runtimeFingerprint(
48
49
  config: BenchmarkDefinition["container"],
50
+ verification?: BenchmarkDefinition["judge"]["verification"],
49
51
  ) {
50
52
  const imageId = await inspectImage(config);
53
+ const verified = await verificationRuntime(verification);
51
54
  return {
52
55
  imageId,
53
56
  candidateHash: fingerprint({
@@ -80,6 +83,7 @@ export async function runtimeFingerprint(
80
83
  ),
81
84
  }),
82
85
  judgeHash: await judgingFingerprint(),
86
+ ...(verified ? { verification: verified } : {}),
83
87
  };
84
88
  }
85
89
 
@@ -0,0 +1,112 @@
1
+ const EXPORT = /export\s+const\s+criteria\s*=\s*/;
2
+
3
+ /** Reads judge.ts's `export const criteria = { … }` as data, without executing the judge.
4
+ * Only object, array, string, number, and boolean literals are accepted.
5
+ */
6
+ export async function readCodeCriteria(file: string): Promise<unknown> {
7
+ const transpiler = new Bun.Transpiler({ loader: "ts" }),
8
+ source = transpiler.transformSync(await Bun.file(file).text()),
9
+ start = EXPORT.exec(source);
10
+ if (start)
11
+ return new LiteralParser(source, start.index + start[0].length).value();
12
+ if (transpiler.scan(source).exports.includes("criteria"))
13
+ throw new Error(
14
+ "Declare judge.ts criteria as `export const criteria = { … }`",
15
+ );
16
+ return undefined;
17
+ }
18
+
19
+ class LiteralParser {
20
+ constructor(
21
+ private source: string,
22
+ private index: number,
23
+ ) {}
24
+ private fail(): never {
25
+ throw new Error(
26
+ "judge.ts criteria must be a literal object of names and categories",
27
+ );
28
+ }
29
+ private space() {
30
+ for (;;) {
31
+ const rest = this.source.slice(this.index);
32
+ const skipped = /^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/)/.exec(rest);
33
+ if (!skipped) return;
34
+ this.index += skipped[0].length;
35
+ }
36
+ }
37
+ private peek() {
38
+ this.space();
39
+ return this.source[this.index];
40
+ }
41
+ private take(token: string) {
42
+ if (this.peek() !== token) this.fail();
43
+ this.index++;
44
+ }
45
+ value(): unknown {
46
+ const next = this.peek();
47
+ if (next === "{") return this.object();
48
+ if (next === "[") return this.array();
49
+ if (next === '"' || next === "'" || next === "`") return this.string();
50
+ const literal =
51
+ /^(?:true|false|-?\d+(?:\.\d+)?)(?![\w$])/.exec(
52
+ this.source.slice(this.index),
53
+ )?.[0];
54
+ if (!literal) this.fail();
55
+ this.index += literal.length;
56
+ return literal === "true" ? true : literal === "false" ? false : Number(literal);
57
+ }
58
+ private object() {
59
+ this.take("{");
60
+ const value: Record<string, unknown> = {};
61
+ while (this.peek() !== "}") {
62
+ const key =
63
+ this.peek() === '"' || this.peek() === "'"
64
+ ? this.string()
65
+ : this.identifier();
66
+ this.take(":");
67
+ value[key] = this.value();
68
+ if (this.peek() !== ",") break;
69
+ this.index++;
70
+ }
71
+ this.take("}");
72
+ return value;
73
+ }
74
+ private array() {
75
+ this.take("[");
76
+ const value: unknown[] = [];
77
+ while (this.peek() !== "]") {
78
+ value.push(this.value());
79
+ if (this.peek() !== ",") break;
80
+ this.index++;
81
+ }
82
+ this.take("]");
83
+ return value;
84
+ }
85
+ private identifier() {
86
+ const name = /^[A-Za-z_$][\w$]*/.exec(this.source.slice(this.index))?.[0];
87
+ if (!name) this.fail();
88
+ this.index += name.length;
89
+ return name;
90
+ }
91
+ private string() {
92
+ const quote = this.source[this.index]!;
93
+ let value = "";
94
+ for (this.index++; this.index < this.source.length; this.index++) {
95
+ const char = this.source[this.index]!;
96
+ if (char === quote) {
97
+ this.index++;
98
+ return value;
99
+ }
100
+ if (quote === "`" && char === "$" && this.source[this.index + 1] === "{")
101
+ this.fail();
102
+ if (char === "\\") {
103
+ const escaped = this.source[++this.index]!;
104
+ value +=
105
+ { n: "\n", t: "\t", r: "\r", "0": "\0" }[escaped] ?? escaped;
106
+ continue;
107
+ }
108
+ value += char;
109
+ }
110
+ this.fail();
111
+ }
112
+ }