@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.
- package/JUDGING.md +63 -1
- package/README.md +4 -0
- package/VERIFICATION.md +100 -0
- package/package.json +2 -2
- package/src/app/grade-recording.ts +16 -0
- package/src/app/input-fingerprints.ts +8 -2
- package/src/app/judge-evidence.ts +8 -2
- package/src/app/judge-run.ts +10 -0
- package/src/app/load-benchmark.ts +102 -9
- package/src/app/prepare-inputs.ts +40 -0
- package/src/app/read-results.ts +23 -5
- package/src/app/rejudge.ts +5 -0
- package/src/app/run-benchmark.ts +36 -4
- package/src/app/scores.ts +19 -2
- package/src/app/serve-results.ts +14 -0
- package/src/cli.ts +27 -2
- package/src/criterion-categories.ts +53 -0
- package/src/index.ts +13 -0
- package/src/infra/containers/catalog.ts +77 -21
- package/src/infra/containers/oci.ts +4 -0
- package/src/infra/judging/code-criteria.ts +112 -0
- package/src/infra/judging/code-result.ts +21 -3
- package/src/infra/judging/code-source.ts +10 -2
- package/src/infra/judging/code-worker.ts +5 -3
- package/src/infra/judging/code.ts +19 -3
- package/src/infra/judging/contract.ts +10 -2
- package/src/infra/judging/index.ts +1 -0
- package/src/infra/recording/index.ts +14 -2
- package/src/infra/verification/image.ts +33 -0
- package/src/infra/verification/runtime/Dockerfile +18 -0
- package/src/infra/verification/runtime/bun.lock +16 -0
- package/src/infra/verification/runtime/package.json +5 -0
- package/src/infra/verification/session.ts +199 -0
- package/src/judge-context.ts +66 -0
- package/src/types.ts +22 -2
- package/src/view.ts +59 -0
- package/viewer/assets/{abnfDiagram-VCTEODGH-Cl3HgjMn.js → abnfDiagram-VCTEODGH-tXaAmCdr.js} +1 -1
- package/viewer/assets/{angular-html-DD_Qs90j.js → angular-html-CNhJPCRr.js} +1 -1
- package/viewer/assets/{angular-ts-C6uwolJ0.js → angular-ts-BnGgGecY.js} +1 -1
- package/viewer/assets/{apl-DXXNG1AP.js → apl-ZOvmk1hE.js} +1 -1
- package/viewer/assets/{arc-VljIODVM.js → arc-BalFY-0o.js} +1 -1
- package/viewer/assets/architecture-7GRP2DOG-Dwn_SxkP.js +1 -0
- package/viewer/assets/{architectureDiagram-5GKGNRK7-BBPZQ1uU.js → architectureDiagram-5GKGNRK7-DpSDAEgb.js} +1 -1
- package/viewer/assets/{astro-DZiA762t.js → astro-BJzTFZ3R.js} +1 -1
- package/viewer/assets/{blade-0_CSK9rb.js → blade-CvS8_w1j.js} +1 -1
- package/viewer/assets/{blockDiagram-I7D4REHJ-C3jAen-I.js → blockDiagram-I7D4REHJ-BY3upSR4.js} +1 -1
- package/viewer/assets/{c-Dr4MdHFU.js → c-D5ZSBtUG.js} +1 -1
- package/viewer/assets/{c4Diagram-7LVT6UL2-x9-A2uwu.js → c4Diagram-7LVT6UL2-CA9keRc2.js} +1 -1
- package/viewer/assets/channel-CgM1_6P7.js +1 -0
- package/viewer/assets/{chapel-Bzsv5OV_.js → chapel-1aYDxJri.js} +1 -1
- package/viewer/assets/{chunk-4HAMMTFA-DhJ5vlqv.js → chunk-4HAMMTFA-B0uITxv4.js} +1 -1
- package/viewer/assets/{chunk-75Z2AOVW-BiWuU3A8.js → chunk-75Z2AOVW-CbTHx8Lz.js} +1 -1
- package/viewer/assets/{chunk-DU6HZSFF-DQsE2_KX.js → chunk-DU6HZSFF-C7mZpnch.js} +1 -1
- package/viewer/assets/{chunk-F27PBJKO-eYpvp3yv.js → chunk-F27PBJKO-iUZG9l9x.js} +1 -1
- package/viewer/assets/{chunk-GMAD6QVW-Cuv8q7cp.js → chunk-GMAD6QVW-BYg8qa71.js} +1 -1
- package/viewer/assets/{chunk-GVQU2GXP-C80TX_-9.js → chunk-GVQU2GXP-rnsdTI4Q.js} +1 -1
- package/viewer/assets/{chunk-IMKFNOWR-D6UIvU6O.js → chunk-IMKFNOWR-D0e2NIBw.js} +1 -1
- package/viewer/assets/{chunk-L3NEJ4N5-B97oN3Wz.js → chunk-L3NEJ4N5-Itlw7HBQ.js} +1 -1
- package/viewer/assets/{chunk-OSK3NFVY-Dv9V7Vwg.js → chunk-OSK3NFVY-DjAAQhPw.js} +1 -1
- package/viewer/assets/{chunk-P2QGCYS3-CCWmLNYJ.js → chunk-P2QGCYS3-XOxrUS-o.js} +1 -1
- package/viewer/assets/{chunk-POPQ4Y6H-B1p9ouPt.js → chunk-POPQ4Y6H-SHuwgciA.js} +1 -1
- package/viewer/assets/{chunk-PWAF6VOD-DA8KGNWy.js → chunk-PWAF6VOD-BtrRW-Sl.js} +1 -1
- package/viewer/assets/{chunk-SHT3W25Y-BrG5_y-x.js → chunk-SHT3W25Y-6Pacfssp.js} +1 -1
- package/viewer/assets/{chunk-SVP7TREG-BfR4k1NG.js → chunk-SVP7TREG-eTVS9a20.js} +1 -1
- package/viewer/assets/{chunk-TICWLB2K-Cl_FiqB8.js → chunk-TICWLB2K-CPnAfOZj.js} +1 -1
- package/viewer/assets/{chunk-XXDRQBXY-D4qiShQC.js → chunk-XXDRQBXY-ciXhM2Hb.js} +1 -1
- package/viewer/assets/classDiagram-ZZMXUADV-BtFc7wo5.js +1 -0
- package/viewer/assets/classDiagram-v2-VYDZK3BY-BtFc7wo5.js +1 -0
- package/viewer/assets/{cobol-BnQBc2SA.js → cobol-CEoq_5kf.js} +1 -1
- package/viewer/assets/{coffee-D8jvqPgZ.js → coffee-CZTWsTxw.js} +1 -1
- package/viewer/assets/{cose-bilkent-JH36ORCC-BcmCCZr6.js → cose-bilkent-JH36ORCC-NCkOcFkY.js} +1 -1
- package/viewer/assets/{cpp-MUcJROqQ.js → cpp-Hh-o0ivR.js} +1 -1
- package/viewer/assets/{crystal-CFGJH8of.js → crystal-CL_KTsac.js} +1 -1
- package/viewer/assets/{css-Z5Q-tnUN.js → css-9neLgVLd.js} +1 -1
- package/viewer/assets/{cynefin-OW5HDTMX-Hu_taNlH.js → cynefin-OW5HDTMX-Ba1tlgBB.js} +1 -1
- package/viewer/assets/{cynefinDiagram-5FMLGOSQ-DGCR5Bu-.js → cynefinDiagram-5FMLGOSQ-CMrzWo3p.js} +1 -1
- package/viewer/assets/{dagre-GXQ25YYZ-BONZK_72.js → dagre-GXQ25YYZ-DwjOnUDD.js} +1 -1
- package/viewer/assets/{diagram-S7CK7UJ4-B3PuH0gY.js → diagram-S7CK7UJ4-DA0PXsM6.js} +1 -1
- package/viewer/assets/{diagram-UQ7AKVKN-_nLXEPxU.js → diagram-UQ7AKVKN-C5BBFVvO.js} +1 -1
- package/viewer/assets/{diagram-VSXAHHWV-W8uCnJKR.js → diagram-VSXAHHWV-COgLeOyi.js} +1 -1
- package/viewer/assets/{diagram-VX7I27RA-Dw85ssfk.js → diagram-VX7I27RA-CEu-Pm3B.js} +1 -1
- package/viewer/assets/{diagram-Z3DM3KII-iT_nC6Ci.js → diagram-Z3DM3KII-BGq1_7BE.js} +1 -1
- package/viewer/assets/{dist-CQ0iZX7g.js → dist-CX8-lyNa.js} +1 -1
- package/viewer/assets/{ebnfDiagram-PWID7BFC-CmB3I8gj.js → ebnfDiagram-PWID7BFC-CGoEp9fJ.js} +1 -1
- package/viewer/assets/{edge-ChegMQD3.js → edge-CTz4kCIs.js} +1 -1
- package/viewer/assets/{elixir-BIS6GmoK.js → elixir-Bi8YL7XX.js} +1 -1
- package/viewer/assets/{elm-0b95WpPs.js → elm-C-UxMmSh.js} +1 -1
- package/viewer/assets/{erDiagram-RLTQ6QDP-BdrQ4dDR.js → erDiagram-RLTQ6QDP-Bb0ICmqZ.js} +1 -1
- package/viewer/assets/{erb-B6AHMmNJ.js → erb-ZSq62Y_u.js} +1 -1
- package/viewer/assets/eventmodeling-NTZA5JFV-BCWgnTvv.js +1 -0
- package/viewer/assets/flowDiagram-HODETNUW-B4hhZ1QN.js +1 -0
- package/viewer/assets/{ganttDiagram-EL5Y4UJY-CScNarbN.js → ganttDiagram-EL5Y4UJY-B4KN9ny5.js} +1 -1
- package/viewer/assets/{git-rebase-eLwqxn21.js → git-rebase-Cw9GoTpw.js} +1 -1
- package/viewer/assets/{gitGraph-4MIJSDKK-DBJydFU4.js → gitGraph-4MIJSDKK-CI3xWhLt.js} +1 -1
- package/viewer/assets/{gitGraphDiagram-WWUBYQGX-BNpJTag6.js → gitGraphDiagram-WWUBYQGX-CPv8-MbI.js} +1 -1
- package/viewer/assets/{glimmer-js-BPUd-Hdw.js → glimmer-js-Bb_1emHy.js} +1 -1
- package/viewer/assets/{glimmer-ts-BsY3Gaee.js → glimmer-ts-D2kOXMB0.js} +1 -1
- package/viewer/assets/{glsl-BgZlzkDK.js → glsl-DEqjHGFr.js} +1 -1
- package/viewer/assets/{graphql-BNNbyKev.js → graphql-Bi0OE4k4.js} +1 -1
- package/viewer/assets/{hack-DXqsPyfj.js → hack-aU4VRzRW.js} +1 -1
- package/viewer/assets/{haml-CEIe07b8.js → haml-B-2wwgyn.js} +1 -1
- package/viewer/assets/{handlebars-4TLrUOf9.js → handlebars-Bmq7hbFI.js} +1 -1
- package/viewer/assets/{html-Dirc4JcS.js → html-BBsNbpya.js} +1 -1
- package/viewer/assets/{html-derivative-lYLgmFdz.js → html-derivative-CkVRX-vn.js} +1 -1
- package/viewer/assets/{http-BhrBDOp-.js → http-cQ-nDL4L.js} +1 -1
- package/viewer/assets/{hurl-DWBDJqQR.js → hurl-D5phPwZN.js} +1 -1
- package/viewer/assets/{index-w8_YIakp.css → index-DdaXmlLv.css} +1 -1
- package/viewer/assets/index-Dhnl4xDm.js +795 -0
- package/viewer/assets/{info-A6RAGUB7-Cqp6XvkH.js → info-A6RAGUB7-CRDxOY87.js} +1 -1
- package/viewer/assets/{infoDiagram-27XIBGKW-CCvdaWwj.js → infoDiagram-27XIBGKW-D6y32CfO.js} +1 -1
- package/viewer/assets/{ishikawaDiagram-5VMMS53U-DQ4-h-VU.js → ishikawaDiagram-5VMMS53U-BWFBTCh5.js} +1 -1
- package/viewer/assets/{java-CdPCMwep.js → java-CT3DXSYV.js} +1 -1
- package/viewer/assets/{javascript-CvM774KP.js → javascript-BPmsAXaP.js} +1 -1
- package/viewer/assets/{jinja-BGKKdTiv.js → jinja-Ct_36dEc.js} +1 -1
- package/viewer/assets/{jison-CXj9ikNa.js → jison-BYrmy85c.js} +1 -1
- package/viewer/assets/{journeyDiagram-3NMN7TZE-DUzDc5Z8.js → journeyDiagram-3NMN7TZE-6XCObBEx.js} +1 -1
- package/viewer/assets/{json-NHDEs7sd.js → json-BCFP8udh.js} +1 -1
- package/viewer/assets/{jsx-FNvyvbBd.js → jsx-Dgzhn0Dq.js} +1 -1
- package/viewer/assets/{julia-B3kEyS8t.js → julia-DIddcjVY.js} +1 -1
- package/viewer/assets/{just-CpYQ-9KV.js → just-C5t963St.js} +1 -1
- package/viewer/assets/{kanban-definition-UXKFOSKX-ChHjSmhz.js → kanban-definition-UXKFOSKX-DU1B45HL.js} +1 -1
- package/viewer/assets/{latex-OWDJhn4l.js → latex-BKZooW5w.js} +1 -1
- package/viewer/assets/{line-DA6hWqrZ.js → line-lYgs1iW7.js} +1 -1
- package/viewer/assets/{linear-UNQGDr0C.js → linear-sHqbNFGm.js} +1 -1
- package/viewer/assets/{liquid-PySKINiF.js → liquid-_PLVu-8C.js} +1 -1
- package/viewer/assets/{lua-DVWbE3Qa.js → lua-BWNAOGrF.js} +1 -1
- package/viewer/assets/{marko-9EBuCkFn.js → marko-DC7qz2de.js} +1 -1
- package/viewer/assets/{mdc-BlHUSA6C.js → mdc-Bn_2wB99.js} +1 -1
- package/viewer/assets/{mermaid-parser.core-86Qkc0ID.js → mermaid-parser.core-jzj7JZTj.js} +3 -3
- package/viewer/assets/{mermaid.core-DaVdMmkQ.js → mermaid.core-DpRyWAPv.js} +4 -4
- package/viewer/assets/{mindmap-definition-YA3MSWOX-oloAEnnx.js → mindmap-definition-YA3MSWOX-CIZjqKbE.js} +1 -1
- package/viewer/assets/{nginx-CDJjPEvc.js → nginx-Ce7JLwNg.js} +1 -1
- package/viewer/assets/{nim-DNAK-c_E.js → nim-CP_MVph6.js} +1 -1
- package/viewer/assets/{org-D6PZe7Hw.js → org-BVKft-Jg.js} +1 -1
- package/viewer/assets/{packet-AYTQ26CC-CF1AN50P.js → packet-AYTQ26CC-BwRTCCV1.js} +1 -1
- package/viewer/assets/{pegDiagram-XKGWAZYB-DGz2-yRO.js → pegDiagram-XKGWAZYB-3nilNfrm.js} +1 -1
- package/viewer/assets/{perl-DIspB-Mg.js → perl-DRhmHTw_.js} +1 -1
- package/viewer/assets/{php-BVepTqtk.js → php-CrnivzIK.js} +1 -1
- package/viewer/assets/{pie-WAS4IAKB-Cx1FR1mr.js → pie-WAS4IAKB-Dn0ztV-V.js} +1 -1
- package/viewer/assets/{pieDiagram-E7YTZNPT-BbbWhyZI.js → pieDiagram-E7YTZNPT-BFj69QcO.js} +1 -1
- package/viewer/assets/{pug-DAPcGjX6.js → pug-BfP-YwOD.js} +1 -1
- package/viewer/assets/{qml-DWWtxGbA.js → qml-BHf9C17M.js} +1 -1
- package/viewer/assets/{quadrantDiagram-AXDQQJYC-DrUJjgA1.js → quadrantDiagram-AXDQQJYC-2t5I-_4_.js} +1 -1
- package/viewer/assets/{r-Mpv0S460.js → r-DjPtVJdp.js} +1 -1
- package/viewer/assets/{radar-RG4KPBEZ-y9SAfPV_.js → radar-RG4KPBEZ-D14Z-9c0.js} +1 -1
- package/viewer/assets/{railroad-74A4TZTK-Dfc0Q6_w.js → railroad-74A4TZTK-BuxyK73P.js} +1 -1
- package/viewer/assets/railroad-abnf-HS5TGJTU-BM7gtNXd.js +1 -0
- package/viewer/assets/railroad-ebnf-LZEXJU2U-DPlBcoln.js +1 -0
- package/viewer/assets/railroad-peg-WCYAUIDC-B6tOGY79.js +1 -0
- package/viewer/assets/{railroadDiagram-O6MQD6OU-Koe-8k88.js → railroadDiagram-O6MQD6OU-Dm2mv6N0.js} +1 -1
- package/viewer/assets/{razor-1HRaok3B.js → razor-BH3XX51e.js} +1 -1
- package/viewer/assets/{regexp-dtCIuY15.js → regexp-DPzTmgy2.js} +1 -1
- package/viewer/assets/{requirementDiagram-BXWQKSXE-BkisH0SS.js → requirementDiagram-BXWQKSXE-BJTZ8ml9.js} +1 -1
- package/viewer/assets/{rst-DHb1VRTR.js → rst-DQbfRM9X.js} +1 -1
- package/viewer/assets/{ruby-BFIUXRbM.js → ruby-DxybHg9D.js} +1 -1
- package/viewer/assets/{sankeyDiagram-P5KCCOFB-B7LDcgGb.js → sankeyDiagram-P5KCCOFB-eBLIKN_A.js} +1 -1
- package/viewer/assets/{sas-CXVQStV2.js → sas-DoUzRWOs.js} +1 -1
- package/viewer/assets/{scss-D1TWv7v-.js → scss-BSqQF9Aq.js} +1 -1
- package/viewer/assets/{sequenceDiagram-WJ2MYXX4-CVdRHXxu.js → sequenceDiagram-WJ2MYXX4-BxHxbxqV.js} +1 -1
- package/viewer/assets/{shellscript-BNxbdlcG.js → shellscript-DkZJ1HjW.js} +1 -1
- package/viewer/assets/{shellsession-BPgWxeMC.js → shellsession-ykUTtcwM.js} +1 -1
- package/viewer/assets/{soy-xvxZ4puZ.js → soy-4rmF0gor.js} +1 -1
- package/viewer/assets/{sql-mcloyhsl.js → sql-wJX-vNm_.js} +1 -1
- package/viewer/assets/{src-CEugxhjf.js → src-IhQtBUXO.js} +1 -1
- package/viewer/assets/{stata-BH9WxX3M.js → stata-Bjw5vh7C.js} +1 -1
- package/viewer/assets/{stateDiagram-D77RDMKH-DCe7c0bj.js → stateDiagram-D77RDMKH-DNPBpLlw.js} +1 -1
- package/viewer/assets/stateDiagram-v2-MP3YSRHH-sLT-i9tN.js +1 -0
- package/viewer/assets/{surrealql-DT6z9HjU.js → surrealql-B-cZs35d.js} +1 -1
- package/viewer/assets/{svelte-DhMjoL5f.js → svelte-BtYh0aTf.js} +1 -1
- package/viewer/assets/{swimlanes-42K2YHIH-0HmmIS3k.js → swimlanes-42K2YHIH-CnHbZ76W.js} +1 -1
- package/viewer/assets/swimlanesDiagram-VR7AAH4N-DIpLefsN.js +8 -0
- package/viewer/assets/{templ-DommSy4z.js → templ-eYWdvrUr.js} +1 -1
- package/viewer/assets/{tex-CaFWJFN-.js → tex-DQ-rt_Oj.js} +1 -1
- package/viewer/assets/{timeline-definition-24CTP7MA-od0g4NnC.js → timeline-definition-24CTP7MA-vOTFZgf6.js} +1 -1
- package/viewer/assets/{treeView-Q6P3EWNA-BI6VSGO-.js → treeView-Q6P3EWNA-CU_mSunf.js} +1 -1
- package/viewer/assets/{treemap-WGGIJYW6-Bk8N6Y68.js → treemap-WGGIJYW6-BbLri-XL.js} +1 -1
- package/viewer/assets/{ts-tags-BYVx2WcX.js → ts-tags-CspMWJ3s.js} +1 -1
- package/viewer/assets/{tsx-nHQYkkGG.js → tsx-CXUmELmz.js} +1 -1
- package/viewer/assets/{twig-Cw-lWIlb.js → twig-B0yioFX1.js} +1 -1
- package/viewer/assets/{typescript-DygBWTVv.js → typescript-vMN-etbi.js} +1 -1
- package/viewer/assets/{typst-CGHo10wY.js → typst-Cvk0bEbK.js} +1 -1
- package/viewer/assets/{vennDiagram-4TSXK5OY-DH3Uf812.js → vennDiagram-4TSXK5OY-PBkXHE6T.js} +1 -1
- package/viewer/assets/{vue-CX1DrRJy.js → vue-Ce21QNcM.js} +1 -1
- package/viewer/assets/{vue-html-c2PT7ajU.js → vue-html-DsNf3O2s.js} +1 -1
- package/viewer/assets/{vue-vine-R8BVuMuN.js → vue-vine-BsGtGXV3.js} +1 -1
- package/viewer/assets/{wardley-WFR3VGLG-DaBE6t7F.js → wardley-WFR3VGLG-kwPRtpGA.js} +1 -1
- package/viewer/assets/{wardleyDiagram-VM6X3IG4-CpuxvD_D.js → wardleyDiagram-VM6X3IG4-BacMGxmX.js} +1 -1
- package/viewer/assets/{xml-CtozFVgZ.js → xml-BmjAaPhA.js} +1 -1
- package/viewer/assets/{xsl-Bywa5lsK.js → xsl-CtfA1v1E.js} +1 -1
- package/viewer/assets/{xychartDiagram-S5SC5T6Z-CIooxwr4.js → xychartDiagram-S5SC5T6Z-BDIbR1Lm.js} +1 -1
- package/viewer/assets/{yaml-m1ZbeIKu.js → yaml-A9EbijaD.js} +1 -1
- package/viewer/index.html +2 -2
- package/viewer/assets/architecture-7GRP2DOG-cmvaWKJl.js +0 -1
- package/viewer/assets/channel-BrXVTGcv.js +0 -1
- package/viewer/assets/classDiagram-ZZMXUADV-BsgTq0ME.js +0 -1
- package/viewer/assets/classDiagram-v2-VYDZK3BY-BsgTq0ME.js +0 -1
- package/viewer/assets/eventmodeling-NTZA5JFV-D1Zwl9Cr.js +0 -1
- package/viewer/assets/flowDiagram-HODETNUW-Dfw-HgvJ.js +0 -1
- package/viewer/assets/index-LKycHUia.js +0 -795
- package/viewer/assets/railroad-abnf-HS5TGJTU-CB_PJcK6.js +0 -1
- package/viewer/assets/railroad-ebnf-LZEXJU2U-Bwgt_HNz.js +0 -1
- package/viewer/assets/railroad-peg-WCYAUIDC-DkZKEvIQ.js +0 -1
- package/viewer/assets/stateDiagram-v2-MP3YSRHH-DAiJpzC0.js +0 -1
- package/viewer/assets/swimlanesDiagram-VR7AAH4N-rLdQ1dbD.js +0 -8
package/src/app/read-results.ts
CHANGED
|
@@ -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,
|
package/src/app/rejudge.ts
CHANGED
|
@@ -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")
|
package/src/app/run-benchmark.ts
CHANGED
|
@@ -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 ||
|
|
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
|
-
(!
|
|
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
|
|
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) => [
|
|
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
|
-
|
|
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
|
package/src/app/serve-results.ts
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
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
|
+
}
|