@hona/openeval 0.3.3 → 0.4.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 (182) hide show
  1. package/JUDGING.md +57 -0
  2. package/README.md +4 -0
  3. package/package.json +1 -1
  4. package/src/app/load-benchmark.ts +87 -7
  5. package/src/app/read-results.ts +2 -1
  6. package/src/app/run-benchmark.ts +34 -3
  7. package/src/app/scores.ts +19 -2
  8. package/src/cli.ts +6 -0
  9. package/src/criterion-categories.ts +53 -0
  10. package/src/index.ts +5 -0
  11. package/src/infra/containers/catalog.ts +77 -21
  12. package/src/infra/judging/code-criteria.ts +112 -0
  13. package/src/types.ts +12 -0
  14. package/src/view.ts +59 -0
  15. package/viewer/assets/{abnfDiagram-VCTEODGH-Cl3HgjMn.js → abnfDiagram-VCTEODGH-D-idCGaW.js} +1 -1
  16. package/viewer/assets/{angular-html-DD_Qs90j.js → angular-html-B-7vkhmj.js} +1 -1
  17. package/viewer/assets/{angular-ts-C6uwolJ0.js → angular-ts-CqJWLTIZ.js} +1 -1
  18. package/viewer/assets/{apl-DXXNG1AP.js → apl-DVTjqAhB.js} +1 -1
  19. package/viewer/assets/{arc-VljIODVM.js → arc-Bmz8zsvq.js} +1 -1
  20. package/viewer/assets/architecture-7GRP2DOG-D3Kw34KW.js +1 -0
  21. package/viewer/assets/{architectureDiagram-5GKGNRK7-BBPZQ1uU.js → architectureDiagram-5GKGNRK7-CxqP2ijS.js} +1 -1
  22. package/viewer/assets/{astro-DZiA762t.js → astro-Ih6QH4I8.js} +1 -1
  23. package/viewer/assets/{blade-0_CSK9rb.js → blade-vzkWgA61.js} +1 -1
  24. package/viewer/assets/{blockDiagram-I7D4REHJ-C3jAen-I.js → blockDiagram-I7D4REHJ-Dm35S95Q.js} +1 -1
  25. package/viewer/assets/{c-Dr4MdHFU.js → c-092Q-y5e.js} +1 -1
  26. package/viewer/assets/{c4Diagram-7LVT6UL2-x9-A2uwu.js → c4Diagram-7LVT6UL2-DArgyJNC.js} +1 -1
  27. package/viewer/assets/channel-D2lVv6ST.js +1 -0
  28. package/viewer/assets/{chapel-Bzsv5OV_.js → chapel-DUOt2X3X.js} +1 -1
  29. package/viewer/assets/{chunk-4HAMMTFA-DhJ5vlqv.js → chunk-4HAMMTFA-Bm3UCwQ6.js} +1 -1
  30. package/viewer/assets/{chunk-75Z2AOVW-BiWuU3A8.js → chunk-75Z2AOVW-CpmjsCJX.js} +1 -1
  31. package/viewer/assets/{chunk-DU6HZSFF-DQsE2_KX.js → chunk-DU6HZSFF-DYT2KEwe.js} +1 -1
  32. package/viewer/assets/{chunk-F27PBJKO-eYpvp3yv.js → chunk-F27PBJKO-BFGd2OPj.js} +1 -1
  33. package/viewer/assets/{chunk-GMAD6QVW-Cuv8q7cp.js → chunk-GMAD6QVW-D8gzxWqz.js} +1 -1
  34. package/viewer/assets/{chunk-GVQU2GXP-C80TX_-9.js → chunk-GVQU2GXP-BZJsi-wS.js} +1 -1
  35. package/viewer/assets/{chunk-IMKFNOWR-D6UIvU6O.js → chunk-IMKFNOWR-BKbF8JvM.js} +1 -1
  36. package/viewer/assets/{chunk-L3NEJ4N5-B97oN3Wz.js → chunk-L3NEJ4N5-SWKb6tCy.js} +1 -1
  37. package/viewer/assets/{chunk-OSK3NFVY-Dv9V7Vwg.js → chunk-OSK3NFVY-BrqoFOmR.js} +1 -1
  38. package/viewer/assets/{chunk-P2QGCYS3-CCWmLNYJ.js → chunk-P2QGCYS3-B10Cyxbx.js} +1 -1
  39. package/viewer/assets/{chunk-POPQ4Y6H-B1p9ouPt.js → chunk-POPQ4Y6H-DXpfBPGQ.js} +1 -1
  40. package/viewer/assets/{chunk-PWAF6VOD-DA8KGNWy.js → chunk-PWAF6VOD-DZmaXnfr.js} +1 -1
  41. package/viewer/assets/{chunk-SHT3W25Y-BrG5_y-x.js → chunk-SHT3W25Y-1U6zFGxP.js} +1 -1
  42. package/viewer/assets/{chunk-SVP7TREG-BfR4k1NG.js → chunk-SVP7TREG-CaW6KcpM.js} +1 -1
  43. package/viewer/assets/{chunk-TICWLB2K-Cl_FiqB8.js → chunk-TICWLB2K-X4pj7G4S.js} +1 -1
  44. package/viewer/assets/{chunk-XXDRQBXY-D4qiShQC.js → chunk-XXDRQBXY-0afAM3OP.js} +1 -1
  45. package/viewer/assets/classDiagram-ZZMXUADV-DqBNvABf.js +1 -0
  46. package/viewer/assets/classDiagram-v2-VYDZK3BY-DqBNvABf.js +1 -0
  47. package/viewer/assets/{cobol-BnQBc2SA.js → cobol-CtspwbZ4.js} +1 -1
  48. package/viewer/assets/{coffee-D8jvqPgZ.js → coffee-qbHB9gj8.js} +1 -1
  49. package/viewer/assets/{cose-bilkent-JH36ORCC-BcmCCZr6.js → cose-bilkent-JH36ORCC-7s0vcDw1.js} +1 -1
  50. package/viewer/assets/{cpp-MUcJROqQ.js → cpp-Dx37X5A_.js} +1 -1
  51. package/viewer/assets/{crystal-CFGJH8of.js → crystal-CiWjJvk4.js} +1 -1
  52. package/viewer/assets/{css-Z5Q-tnUN.js → css-CLeuyJyb.js} +1 -1
  53. package/viewer/assets/{cynefin-OW5HDTMX-Hu_taNlH.js → cynefin-OW5HDTMX-B4XFGNNZ.js} +1 -1
  54. package/viewer/assets/{cynefinDiagram-5FMLGOSQ-DGCR5Bu-.js → cynefinDiagram-5FMLGOSQ-DdNMKbw6.js} +1 -1
  55. package/viewer/assets/{dagre-GXQ25YYZ-BONZK_72.js → dagre-GXQ25YYZ-BkDo4A91.js} +1 -1
  56. package/viewer/assets/{diagram-S7CK7UJ4-B3PuH0gY.js → diagram-S7CK7UJ4-DU8cnnVy.js} +1 -1
  57. package/viewer/assets/{diagram-UQ7AKVKN-_nLXEPxU.js → diagram-UQ7AKVKN-Dg2emYKa.js} +1 -1
  58. package/viewer/assets/{diagram-VSXAHHWV-W8uCnJKR.js → diagram-VSXAHHWV-C_-YSzcn.js} +1 -1
  59. package/viewer/assets/{diagram-VX7I27RA-Dw85ssfk.js → diagram-VX7I27RA-BLCEnxqe.js} +1 -1
  60. package/viewer/assets/{diagram-Z3DM3KII-iT_nC6Ci.js → diagram-Z3DM3KII-62CpIiAg.js} +1 -1
  61. package/viewer/assets/{dist-CQ0iZX7g.js → dist-CQIgbdem.js} +1 -1
  62. package/viewer/assets/{ebnfDiagram-PWID7BFC-CmB3I8gj.js → ebnfDiagram-PWID7BFC-FkGGETgC.js} +1 -1
  63. package/viewer/assets/{edge-ChegMQD3.js → edge-CzmGSXHr.js} +1 -1
  64. package/viewer/assets/{elixir-BIS6GmoK.js → elixir-DDfD_-oS.js} +1 -1
  65. package/viewer/assets/{elm-0b95WpPs.js → elm-l7aRfQIq.js} +1 -1
  66. package/viewer/assets/{erDiagram-RLTQ6QDP-BdrQ4dDR.js → erDiagram-RLTQ6QDP-GlCpsS1v.js} +1 -1
  67. package/viewer/assets/{erb-B6AHMmNJ.js → erb-qVffN5xM.js} +1 -1
  68. package/viewer/assets/eventmodeling-NTZA5JFV-07UfGVCm.js +1 -0
  69. package/viewer/assets/flowDiagram-HODETNUW-CyIUf7TW.js +1 -0
  70. package/viewer/assets/{ganttDiagram-EL5Y4UJY-CScNarbN.js → ganttDiagram-EL5Y4UJY-CH_Tk4Ex.js} +1 -1
  71. package/viewer/assets/{git-rebase-eLwqxn21.js → git-rebase-jEJ8nKk1.js} +1 -1
  72. package/viewer/assets/{gitGraph-4MIJSDKK-DBJydFU4.js → gitGraph-4MIJSDKK-Cc9o1l4Y.js} +1 -1
  73. package/viewer/assets/{gitGraphDiagram-WWUBYQGX-BNpJTag6.js → gitGraphDiagram-WWUBYQGX-UdfHBATc.js} +1 -1
  74. package/viewer/assets/{glimmer-js-BPUd-Hdw.js → glimmer-js-BMk2JCW8.js} +1 -1
  75. package/viewer/assets/{glimmer-ts-BsY3Gaee.js → glimmer-ts-BH7RwQxU.js} +1 -1
  76. package/viewer/assets/{glsl-BgZlzkDK.js → glsl-CAWrZoEq.js} +1 -1
  77. package/viewer/assets/{graphql-BNNbyKev.js → graphql-Bac_hecs.js} +1 -1
  78. package/viewer/assets/{hack-DXqsPyfj.js → hack-1OgpAtm-.js} +1 -1
  79. package/viewer/assets/{haml-CEIe07b8.js → haml-CvwG3BzW.js} +1 -1
  80. package/viewer/assets/{handlebars-4TLrUOf9.js → handlebars-Cq4oEEp8.js} +1 -1
  81. package/viewer/assets/{html-Dirc4JcS.js → html-D5wZ_JR6.js} +1 -1
  82. package/viewer/assets/{html-derivative-lYLgmFdz.js → html-derivative-ZfzPj0aS.js} +1 -1
  83. package/viewer/assets/{http-BhrBDOp-.js → http-BDE2UDvO.js} +1 -1
  84. package/viewer/assets/{hurl-DWBDJqQR.js → hurl-CQbXNLlk.js} +1 -1
  85. package/viewer/assets/index-BezQIu6a.js +795 -0
  86. package/viewer/assets/{index-w8_YIakp.css → index-OL_rrWNy.css} +1 -1
  87. package/viewer/assets/{info-A6RAGUB7-Cqp6XvkH.js → info-A6RAGUB7-YD13oMfx.js} +1 -1
  88. package/viewer/assets/{infoDiagram-27XIBGKW-CCvdaWwj.js → infoDiagram-27XIBGKW-jYKjA_l7.js} +1 -1
  89. package/viewer/assets/{ishikawaDiagram-5VMMS53U-DQ4-h-VU.js → ishikawaDiagram-5VMMS53U-BBPzHiVd.js} +1 -1
  90. package/viewer/assets/{java-CdPCMwep.js → java-Cbpu4oyT.js} +1 -1
  91. package/viewer/assets/{javascript-CvM774KP.js → javascript-UMuq64YD.js} +1 -1
  92. package/viewer/assets/{jinja-BGKKdTiv.js → jinja-DVvZtqgU.js} +1 -1
  93. package/viewer/assets/{jison-CXj9ikNa.js → jison-CZCXBIV3.js} +1 -1
  94. package/viewer/assets/{journeyDiagram-3NMN7TZE-DUzDc5Z8.js → journeyDiagram-3NMN7TZE-CMqG5ndb.js} +1 -1
  95. package/viewer/assets/{json-NHDEs7sd.js → json-CzTvWngu.js} +1 -1
  96. package/viewer/assets/{jsx-FNvyvbBd.js → jsx-CNJnEGR4.js} +1 -1
  97. package/viewer/assets/{julia-B3kEyS8t.js → julia-Bj0q4uoH.js} +1 -1
  98. package/viewer/assets/{just-CpYQ-9KV.js → just-93-MRGUC.js} +1 -1
  99. package/viewer/assets/{kanban-definition-UXKFOSKX-ChHjSmhz.js → kanban-definition-UXKFOSKX-VyFYPq9b.js} +1 -1
  100. package/viewer/assets/{latex-OWDJhn4l.js → latex-CXd1tMjA.js} +1 -1
  101. package/viewer/assets/{line-DA6hWqrZ.js → line-D5nvVDHp.js} +1 -1
  102. package/viewer/assets/{linear-UNQGDr0C.js → linear-Cq-FJZ_z.js} +1 -1
  103. package/viewer/assets/{liquid-PySKINiF.js → liquid-Cb-ALXSr.js} +1 -1
  104. package/viewer/assets/{lua-DVWbE3Qa.js → lua-BxTQiamn.js} +1 -1
  105. package/viewer/assets/{marko-9EBuCkFn.js → marko-mFYyU5jn.js} +1 -1
  106. package/viewer/assets/{mdc-BlHUSA6C.js → mdc-Oryqox7_.js} +1 -1
  107. package/viewer/assets/{mermaid-parser.core-86Qkc0ID.js → mermaid-parser.core-CCDsanlH.js} +3 -3
  108. package/viewer/assets/{mermaid.core-DaVdMmkQ.js → mermaid.core-Crd1YMgi.js} +4 -4
  109. package/viewer/assets/{mindmap-definition-YA3MSWOX-oloAEnnx.js → mindmap-definition-YA3MSWOX-BKLrEFWt.js} +1 -1
  110. package/viewer/assets/{nginx-CDJjPEvc.js → nginx-C1PwWP7d.js} +1 -1
  111. package/viewer/assets/{nim-DNAK-c_E.js → nim-PszXW-l4.js} +1 -1
  112. package/viewer/assets/{org-D6PZe7Hw.js → org-DO0CuJlO.js} +1 -1
  113. package/viewer/assets/{packet-AYTQ26CC-CF1AN50P.js → packet-AYTQ26CC-BNPwLE6g.js} +1 -1
  114. package/viewer/assets/{pegDiagram-XKGWAZYB-DGz2-yRO.js → pegDiagram-XKGWAZYB-DjBg_iPh.js} +1 -1
  115. package/viewer/assets/{perl-DIspB-Mg.js → perl-CHhAgoDL.js} +1 -1
  116. package/viewer/assets/{php-BVepTqtk.js → php-CA4H6qnu.js} +1 -1
  117. package/viewer/assets/{pie-WAS4IAKB-Cx1FR1mr.js → pie-WAS4IAKB-BzNmHX-Y.js} +1 -1
  118. package/viewer/assets/{pieDiagram-E7YTZNPT-BbbWhyZI.js → pieDiagram-E7YTZNPT-cgMB-fBw.js} +1 -1
  119. package/viewer/assets/{pug-DAPcGjX6.js → pug-DDuKTe7C.js} +1 -1
  120. package/viewer/assets/{qml-DWWtxGbA.js → qml-DSd2VypD.js} +1 -1
  121. package/viewer/assets/{quadrantDiagram-AXDQQJYC-DrUJjgA1.js → quadrantDiagram-AXDQQJYC-Crzhs7Np.js} +1 -1
  122. package/viewer/assets/{r-Mpv0S460.js → r-qf-5qR5Q.js} +1 -1
  123. package/viewer/assets/{radar-RG4KPBEZ-y9SAfPV_.js → radar-RG4KPBEZ-B3oI70pB.js} +1 -1
  124. package/viewer/assets/{railroad-74A4TZTK-Dfc0Q6_w.js → railroad-74A4TZTK-DOM5Od17.js} +1 -1
  125. package/viewer/assets/railroad-abnf-HS5TGJTU-ScZ2h_5Q.js +1 -0
  126. package/viewer/assets/railroad-ebnf-LZEXJU2U-_Cl5zxJ-.js +1 -0
  127. package/viewer/assets/railroad-peg-WCYAUIDC-CRlGg7vV.js +1 -0
  128. package/viewer/assets/{railroadDiagram-O6MQD6OU-Koe-8k88.js → railroadDiagram-O6MQD6OU-M3SvSYf-.js} +1 -1
  129. package/viewer/assets/{razor-1HRaok3B.js → razor-B9n9DtIL.js} +1 -1
  130. package/viewer/assets/{regexp-dtCIuY15.js → regexp-DhGN0EOR.js} +1 -1
  131. package/viewer/assets/{requirementDiagram-BXWQKSXE-BkisH0SS.js → requirementDiagram-BXWQKSXE-CFisiZTo.js} +1 -1
  132. package/viewer/assets/{rst-DHb1VRTR.js → rst-D1SdhuXd.js} +1 -1
  133. package/viewer/assets/{ruby-BFIUXRbM.js → ruby-BybsgZgf.js} +1 -1
  134. package/viewer/assets/{sankeyDiagram-P5KCCOFB-B7LDcgGb.js → sankeyDiagram-P5KCCOFB-BKf2TMWg.js} +1 -1
  135. package/viewer/assets/{sas-CXVQStV2.js → sas-Io7QwCtD.js} +1 -1
  136. package/viewer/assets/{scss-D1TWv7v-.js → scss-D6XY0yo3.js} +1 -1
  137. package/viewer/assets/{sequenceDiagram-WJ2MYXX4-CVdRHXxu.js → sequenceDiagram-WJ2MYXX4-CHawNQZ9.js} +1 -1
  138. package/viewer/assets/{shellscript-BNxbdlcG.js → shellscript-DSk8kvCh.js} +1 -1
  139. package/viewer/assets/{shellsession-BPgWxeMC.js → shellsession-Be1CN8DO.js} +1 -1
  140. package/viewer/assets/{soy-xvxZ4puZ.js → soy-CGkkqGyT.js} +1 -1
  141. package/viewer/assets/{sql-mcloyhsl.js → sql-DIgb716V.js} +1 -1
  142. package/viewer/assets/{src-CEugxhjf.js → src-DP6Z6M4G.js} +1 -1
  143. package/viewer/assets/{stata-BH9WxX3M.js → stata-CS_2tb9p.js} +1 -1
  144. package/viewer/assets/{stateDiagram-D77RDMKH-DCe7c0bj.js → stateDiagram-D77RDMKH-Cx9Rf6fY.js} +1 -1
  145. package/viewer/assets/stateDiagram-v2-MP3YSRHH-DRb-6t_S.js +1 -0
  146. package/viewer/assets/{surrealql-DT6z9HjU.js → surrealql-Do4o8w1B.js} +1 -1
  147. package/viewer/assets/{svelte-DhMjoL5f.js → svelte-3bKxeV6-.js} +1 -1
  148. package/viewer/assets/{swimlanes-42K2YHIH-0HmmIS3k.js → swimlanes-42K2YHIH-yL2oIw_D.js} +1 -1
  149. package/viewer/assets/swimlanesDiagram-VR7AAH4N-cDiCT6xi.js +8 -0
  150. package/viewer/assets/{templ-DommSy4z.js → templ-BgEYPNGP.js} +1 -1
  151. package/viewer/assets/{tex-CaFWJFN-.js → tex-D_p6whQw.js} +1 -1
  152. package/viewer/assets/{timeline-definition-24CTP7MA-od0g4NnC.js → timeline-definition-24CTP7MA-BJeLq4a5.js} +1 -1
  153. package/viewer/assets/{treeView-Q6P3EWNA-BI6VSGO-.js → treeView-Q6P3EWNA-COcnImiE.js} +1 -1
  154. package/viewer/assets/{treemap-WGGIJYW6-Bk8N6Y68.js → treemap-WGGIJYW6-DpbxWots.js} +1 -1
  155. package/viewer/assets/{ts-tags-BYVx2WcX.js → ts-tags-GBmMk7Oh.js} +1 -1
  156. package/viewer/assets/{tsx-nHQYkkGG.js → tsx-BSD-yNhi.js} +1 -1
  157. package/viewer/assets/{twig-Cw-lWIlb.js → twig-dwAG5SzX.js} +1 -1
  158. package/viewer/assets/{typescript-DygBWTVv.js → typescript-B8YTPl9v.js} +1 -1
  159. package/viewer/assets/{typst-CGHo10wY.js → typst-Dpzojbzo.js} +1 -1
  160. package/viewer/assets/{vennDiagram-4TSXK5OY-DH3Uf812.js → vennDiagram-4TSXK5OY-nF34Gjtn.js} +1 -1
  161. package/viewer/assets/{vue-CX1DrRJy.js → vue-Djbmk2DC.js} +1 -1
  162. package/viewer/assets/{vue-html-c2PT7ajU.js → vue-html-BXc5rfco.js} +1 -1
  163. package/viewer/assets/{vue-vine-R8BVuMuN.js → vue-vine-DHCfQh17.js} +1 -1
  164. package/viewer/assets/{wardley-WFR3VGLG-DaBE6t7F.js → wardley-WFR3VGLG-C1qhMifc.js} +1 -1
  165. package/viewer/assets/{wardleyDiagram-VM6X3IG4-CpuxvD_D.js → wardleyDiagram-VM6X3IG4-B0BfyYWQ.js} +1 -1
  166. package/viewer/assets/{xml-CtozFVgZ.js → xml-CdCEskcV.js} +1 -1
  167. package/viewer/assets/{xsl-Bywa5lsK.js → xsl-BbNukwXP.js} +1 -1
  168. package/viewer/assets/{xychartDiagram-S5SC5T6Z-CIooxwr4.js → xychartDiagram-S5SC5T6Z-DJKYce1U.js} +1 -1
  169. package/viewer/assets/{yaml-m1ZbeIKu.js → yaml-BYLDET4A.js} +1 -1
  170. package/viewer/index.html +2 -2
  171. package/viewer/assets/architecture-7GRP2DOG-cmvaWKJl.js +0 -1
  172. package/viewer/assets/channel-BrXVTGcv.js +0 -1
  173. package/viewer/assets/classDiagram-ZZMXUADV-BsgTq0ME.js +0 -1
  174. package/viewer/assets/classDiagram-v2-VYDZK3BY-BsgTq0ME.js +0 -1
  175. package/viewer/assets/eventmodeling-NTZA5JFV-D1Zwl9Cr.js +0 -1
  176. package/viewer/assets/flowDiagram-HODETNUW-Dfw-HgvJ.js +0 -1
  177. package/viewer/assets/index-LKycHUia.js +0 -795
  178. package/viewer/assets/railroad-abnf-HS5TGJTU-CB_PJcK6.js +0 -1
  179. package/viewer/assets/railroad-ebnf-LZEXJU2U-Bwgt_HNz.js +0 -1
  180. package/viewer/assets/railroad-peg-WCYAUIDC-DkZKEvIQ.js +0 -1
  181. package/viewer/assets/stateDiagram-v2-MP3YSRHH-DAiJpzC0.js +0 -1
  182. package/viewer/assets/swimlanesDiagram-VR7AAH4N-rLdQ1dbD.js +0 -8
package/JUDGING.md CHANGED
@@ -152,6 +152,63 @@ must declare at least one `## Criterion: id — Label`. A normalized Judgment ha
152
152
  an aggregate value and a scores map of CriterionScore objects containing value,
153
153
  reason, evidence, and source. The original code output is retained separately.
154
154
 
155
+ ## Criterion categories
156
+
157
+ A category is any non-empty string on a criterion. Categories group results in
158
+ the viewer and can compose a benchmark. They never change judge input,
159
+ fingerprints, or the headline weighting.
160
+
161
+ In `judge.md`, put one optional line directly below a criterion heading:
162
+
163
+ ```md
164
+ ## Criterion: asked_dialect — Asks for the SQL dialect
165
+ Categories: misalignment, general
166
+ ```
167
+
168
+ OpenEval removes that line before hashing the rubric and before the judge reads
169
+ it, so adding or changing categories does not rejudge recorded evidence. A
170
+ `Categories:` line anywhere else is an error.
171
+
172
+ In `judge.ts`, export labels and categories for the scores it returns:
173
+
174
+ ```ts
175
+ import type { CodeCriteria, JudgeContext } from "@hona/openeval";
176
+
177
+ export const criteria = {
178
+ correct_answer: { name: "Correct answer", categories: ["general"] },
179
+ } satisfies CodeCriteria;
180
+
181
+ export default ({ response }: JudgeContext) => ({
182
+ scores: { correct_answer: response.text.trim() === "42" },
183
+ });
184
+ ```
185
+
186
+ Declare each criterion ID in one judge file only. Names are trimmed and matched
187
+ case-insensitively; the first spelling is displayed. A criterion may have
188
+ several categories, but one is usually clearer. A category score uses the same
189
+ rule as the overall score: criteria are averaged within each eval, then evals
190
+ are weighted equally. Unscored checks keep that category's score a range.
191
+
192
+ Compose a benchmark from categories in `benchmark.ts`. Evals without a matching
193
+ criterion are not run, and scores use only matching criteria:
194
+
195
+ ```ts
196
+ export default {
197
+ models: ["opencode/gpt-6-astra#high"],
198
+ categories: ["coding", "verification"],
199
+ } satisfies Benchmark;
200
+ ```
201
+
202
+ `openeval run --only-category <name>` limits one invocation to evals with a
203
+ matching criterion, like `--only-eval`, without changing the composition.
204
+
205
+ Prefer published category sets so results are comparable:
206
+
207
+ | Kind | Source | Categories |
208
+ | --- | --- | --- |
209
+ | Capability | [Artificial Analysis Intelligence Index](https://artificialanalysis.ai/methodology/intelligence-benchmarking) | `agents`, `coding`, `general`, `scientific-reasoning`; also `multilingual`, `vision` |
210
+ | Agent failure | [MAST](https://arxiv.org/abs/2503.13657) (Cemri et al., NeurIPS 2025) | `specification`, `misalignment`, `verification` |
211
+
155
212
  ## Structured tool submissions
156
213
 
157
214
  | Tool | Data |
package/README.md CHANGED
@@ -86,6 +86,10 @@ or does not provide a parameterized query.
86
86
  | Only asks which database | **1** | **0** |
87
87
  | Required recording is unavailable | **null** | **null** |
88
88
 
89
+ Add an optional `Categories: misalignment` line under a heading to compare models
90
+ by category in the viewer's radar chart and heatmap. Categories never trigger
91
+ rejudging.
92
+
89
93
  → [Write good rubrics](https://openev.al/docs/rubrics/) · [Download the SQL starter](https://openev.al/starter.zip)
90
94
 
91
95
  ## One vocabulary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hona/openeval",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Code and LLM evaluations with isolated agents, recorded OpenCode data, and a results viewer",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,12 +10,17 @@ import type {
10
10
  } from "../types";
11
11
  import { CANDIDATE_TIMEOUT_MS } from "../types";
12
12
  import { rubricCriteria } from "../judgment";
13
+ import {
14
+ categoryKey,
15
+ categoryList,
16
+ rubricCategories,
17
+ } from "../criterion-categories";
13
18
  import { compileCodeJudge } from "../infra/judging/code-source";
19
+ import { readCodeCriteria } from "../infra/judging/code-criteria";
14
20
  import { RUNTIME_IMAGE } from "../infra/opencode/version";
15
21
  import { monitorPolicy } from "./monitor-policy";
16
22
  import {
17
23
  fingerprint,
18
- hash,
19
24
  relativePath,
20
25
  treeHash,
21
26
  contained,
@@ -35,10 +40,13 @@ export const modelRef = (value: unknown): ModelRef => {
35
40
  throw new Error(`Invalid model reference: ${String(value)}`);
36
41
  return value as ModelRef;
37
42
  };
43
+ /** Bun caches modules by path, ignoring URL queries; clear the entry so edits load. */
44
+ async function freshModule(path: string): Promise<Record<string, unknown>> {
45
+ delete require.cache[path];
46
+ return import(pathToFileURL(path).href);
47
+ }
38
48
  async function declaration<T>(path: string): Promise<T> {
39
- const url = pathToFileURL(path);
40
- url.searchParams.set("version", hash(await Bun.file(path).bytes()));
41
- return (await import(url.href)).default;
49
+ return (await freshModule(path)).default as T;
42
50
  }
43
51
  async function loadEval(directory: string): Promise<EvalDefinition> {
44
52
  const id = basename(directory),
@@ -168,10 +176,23 @@ async function loadEval(directory: string): Promise<EvalDefinition> {
168
176
  throw new Error(`${id}: preparation requires argv`);
169
177
  }
170
178
  const promptText = await prompt.text(),
171
- judgeText = hasMarkdown ? await judge.text() : "";
179
+ rubric = rubricCategories(hasMarkdown ? await judge.text() : ""),
180
+ judgeText = rubric.rubric;
172
181
  if (!promptText.trim() || (hasMarkdown && !judgeText.trim()))
173
182
  throw new Error(`${id}: prompt and judge must not be empty`);
174
183
  const code = hasCode ? await compileCodeJudge(codeFile) : undefined;
184
+ const criteria = hasMarkdown ? rubricCriteria(judgeText) : [];
185
+ const declared = hasCode ? await codeCriteria(id, codeFile) : [];
186
+ if (declared.some((item) => criteria.some(({ id }) => id === item.id)))
187
+ throw new Error(
188
+ `${id}: declare each criterion in either judge.md or judge.ts, not both`,
189
+ );
190
+ const categories = Object.fromEntries(
191
+ [
192
+ ...Object.entries(rubric.categories),
193
+ ...declared.map((item) => [item.id, item.categories] as const),
194
+ ].filter(([, names]) => names.length),
195
+ );
175
196
  return {
176
197
  id,
177
198
  directory,
@@ -183,12 +204,49 @@ async function loadEval(directory: string): Promise<EvalDefinition> {
183
204
  source,
184
205
  }),
185
206
  judgeHash: fingerprint({ rubric: judgeText, code: code?.hash }),
186
- criteria: hasMarkdown ? rubricCriteria(judgeText) : [],
207
+ criteria,
208
+ ...(declared.length
209
+ ? { codeCriteria: declared.map(({ id, name }) => ({ id, name })) }
210
+ : {}),
211
+ ...(Object.keys(categories).length ? { categories } : {}),
187
212
  ...(code ? { code } : {}),
188
213
  name: /^# (.+)$/m.exec(judgeText)?.[1] ?? id,
189
214
  };
190
215
  }
191
216
 
217
+ /** Reads judge.ts's optional `criteria` export as data; planning never executes judge code. */
218
+ async function codeCriteria(id: string, path: string) {
219
+ const declared = await readCodeCriteria(path);
220
+ if (declared === undefined) return [];
221
+ if (!declared || typeof declared !== "object" || Array.isArray(declared))
222
+ throw new Error(`${id}/judge.ts criteria must be an object`);
223
+ return Object.entries(declared).map(([criterion, value]) => {
224
+ const where = `${id}/judge.ts criterion ${criterion}`;
225
+ if (!/^[a-z][a-z0-9_]*$/.test(criterion))
226
+ throw new Error(`${where}: use a lowercase snake_case ID`);
227
+ if (
228
+ !value ||
229
+ typeof value !== "object" ||
230
+ Array.isArray(value) ||
231
+ Object.keys(value).some((key) => !["name", "categories"].includes(key))
232
+ )
233
+ throw new Error(`${where}: declare only name and categories`);
234
+ const { name, categories = [] } = value as {
235
+ name?: unknown;
236
+ categories?: unknown;
237
+ };
238
+ if (name !== undefined && (typeof name !== "string" || !name.trim()))
239
+ throw new Error(`${where}: name must be a non-empty string`);
240
+ if (!Array.isArray(categories))
241
+ throw new Error(`${where}: categories must be an array`);
242
+ return {
243
+ id: criterion,
244
+ name: (name as string | undefined)?.trim() ?? criterion.replaceAll("_", " "),
245
+ categories: categoryList(categories, where),
246
+ };
247
+ });
248
+ }
249
+
192
250
  export async function loadBenchmark(
193
251
  path: string,
194
252
  ): Promise<BenchmarkDefinition> {
@@ -214,10 +272,19 @@ export async function loadBenchmark(
214
272
  "concurrency",
215
273
  "candidate",
216
274
  "container",
275
+ "categories",
217
276
  ].includes(key),
218
277
  )
219
278
  )
220
279
  throw new Error("benchmark.ts contains unsupported settings");
280
+ if (
281
+ definition.categories !== undefined &&
282
+ (!Array.isArray(definition.categories) || !definition.categories.length)
283
+ )
284
+ throw new Error("benchmark.ts categories must be a non-empty array");
285
+ const categories = definition.categories
286
+ ? categoryList(definition.categories, "benchmark.ts")
287
+ : undefined;
221
288
  const models = definition.models.map(modelRef);
222
289
  if (
223
290
  definition.judge !== undefined &&
@@ -245,9 +312,21 @@ export async function loadBenchmark(
245
312
  .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
246
313
  .sort((a, b) => a.name.localeCompare(b.name));
247
314
  if (!directories.length) throw new Error("Benchmark has no eval folders");
248
- const evals = await Promise.all(
315
+ const declared = await Promise.all(
249
316
  directories.map((entry) => loadEval(resolve(evalRoot, entry.name))),
250
317
  );
318
+ const keys = categories?.map(categoryKey);
319
+ const evals = keys
320
+ ? declared.filter((item) =>
321
+ Object.values(item.categories ?? {}).some((names) =>
322
+ names.some((name) => keys.includes(categoryKey(name))),
323
+ ),
324
+ )
325
+ : declared;
326
+ if (!evals.length)
327
+ throw new Error(
328
+ `No criteria match the benchmark categories: ${categories!.join(", ")}`,
329
+ );
251
330
  if (evals.some((item) => item.judge) && !definition.judge?.model)
252
331
  throw new Error("A benchmark containing judge.md requires judge.model");
253
332
  const concurrency = positive(definition.concurrency, 10, "Concurrency");
@@ -295,5 +374,6 @@ export async function loadBenchmark(
295
374
  cpus: positive(definition.container?.cpus, 2, "CPU count"),
296
375
  memoryMiB: positive(definition.container?.memoryMiB, 4096, "Memory"),
297
376
  },
377
+ ...(categories ? { categories } : {}),
298
378
  };
299
379
  }
@@ -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 {
@@ -316,6 +316,7 @@ export class ResultReader {
316
316
  evals.map((item) => [item.id, item.name]),
317
317
  ),
318
318
  modelNames: run.modelNames ?? {},
319
+ categories: categoryCatalog(scores[0]?.components ?? []),
319
320
  cost: evalId ? evalCosts[evalId] : sumCosts(Object.values(evalCosts)),
320
321
  evalCosts,
321
322
  runtime,
@@ -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,6 +171,7 @@ 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)
@@ -192,11 +223,11 @@ export async function runBenchmark(
192
223
  results.slots().map((slot) => [slot.id, slot]),
193
224
  );
194
225
  let plan = planBenchmark(definition, runtime, results);
195
- if (selected || options.onlyEvals || options.onlyRepetitions) {
226
+ if (selected || onlyEvals || options.onlyRepetitions) {
196
227
  const retained = new Map(results.slots().map((slot) => [slot.id, slot]));
197
228
  plan = plan.map((item) =>
198
229
  ((!selected || selected.includes(item.slot.model)) &&
199
- (!options.onlyEvals || options.onlyEvals.includes(item.slot.evalId)) &&
230
+ (!onlyEvals || onlyEvals.includes(item.slot.evalId)) &&
200
231
  (!options.onlyRepetitions ||
201
232
  options.onlyRepetitions.includes(item.slot.repetition))) ||
202
233
  item.action === "reuse"
@@ -244,7 +275,7 @@ export async function runBenchmark(
244
275
  execution: {
245
276
  startedAt: now,
246
277
  onlyModels: selected,
247
- onlyEvals: options.onlyEvals,
278
+ onlyEvals,
248
279
  onlyRepetitions: options.onlyRepetitions,
249
280
  budgetUSD: options.maxCostUSD,
250
281
  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
package/src/cli.ts CHANGED
@@ -51,6 +51,7 @@ Options:
51
51
  --model <provider/id> Model to add or remove (repeatable)
52
52
  --only-model <ref> Execute only this model (repeatable)
53
53
  --only-eval <id> Execute only this eval (repeatable)
54
+ --only-category <name> Execute only evals with criteria in this category (repeatable)
54
55
  --only-repetition <n> Execute only this repetition (repeatable)
55
56
  --max-cost <usd> Scheduling budget for this invocation
56
57
  --final-only Judge only after candidates finish
@@ -84,6 +85,11 @@ Requires Bun 1.4.2+, Docker, and an authenticated OpenCode installation.`);
84
85
  arg === "--only-eval" ? [args[index + 1]] : [],
85
86
  )
86
87
  : undefined,
88
+ onlyCategories: args.includes("--only-category")
89
+ ? args.flatMap((arg, index) =>
90
+ arg === "--only-category" ? [args[index + 1]] : [],
91
+ )
92
+ : undefined,
87
93
  maxCostUSD: args.includes("--max-cost")
88
94
  ? Number(option("--max-cost"))
89
95
  : 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,
@@ -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
+ }