reeval 0.11.0 → 0.13.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 (312) hide show
  1. package/README.md +115 -257
  2. package/assets/package.json +1 -1
  3. package/assets/pnpm-lock.yaml +41 -30
  4. package/bin/mcp-timer.cjs +224 -0
  5. package/dist/{adapter-Bwy3QxAK.js → adapter-IKKo4sZj.js} +4 -4
  6. package/dist/{adapter-Bwy3QxAK.js.map → adapter-IKKo4sZj.js.map} +1 -1
  7. package/dist/{agent-DpE80WPz.js → agent-ClE-g9VL.js} +22 -7
  8. package/dist/{agent-DpE80WPz.js.map → agent-ClE-g9VL.js.map} +1 -1
  9. package/dist/{analysis-Bxub9LMT.js → analysis-B0nOfAzA.js} +3 -3
  10. package/dist/{analysis-Bxub9LMT.js.map → analysis-B0nOfAzA.js.map} +1 -1
  11. package/dist/{assemble-D9GWr-HS.js → assemble-BO93VAk1.js} +22 -20
  12. package/dist/assemble-BO93VAk1.js.map +1 -0
  13. package/dist/assemble-VQxyoDxK.js +1 -0
  14. package/dist/{attempt-CGPV3BZY.js → attempt-jI0OWxlz.js} +2 -2
  15. package/dist/{attempt-CGPV3BZY.js.map → attempt-jI0OWxlz.js.map} +1 -1
  16. package/dist/bridge/index.mjs +2 -1
  17. package/dist/{capture-BQlGHiMC.js → capture-dn2hjSLj.js} +29 -21
  18. package/dist/{capture-BQlGHiMC.js.map → capture-dn2hjSLj.js.map} +1 -1
  19. package/dist/capture-nIJ-7kUE.js +1 -0
  20. package/dist/{cli-ClpPT8kn.js → cli-BCE1xlm4.js} +176 -120
  21. package/dist/cli-BCE1xlm4.js.map +1 -0
  22. package/dist/{cli-spec-qmEnQ25_.js → cli-spec-BXIBUlaK.js} +34 -15
  23. package/dist/cli-spec-BXIBUlaK.js.map +1 -0
  24. package/dist/cli.js +2 -2
  25. package/dist/{command-BlM0y8jN.js → command-Bh3yhN_b.js} +8 -8
  26. package/dist/{command-BlM0y8jN.js.map → command-Bh3yhN_b.js.map} +1 -1
  27. package/dist/command-C6vwmjS7.js +91 -0
  28. package/dist/command-C6vwmjS7.js.map +1 -0
  29. package/dist/{command-DbAe1Rzb.js → command-CANun0m0.js} +16 -19
  30. package/dist/command-CANun0m0.js.map +1 -0
  31. package/dist/command-CBaHukyl.js +335 -0
  32. package/dist/command-CBaHukyl.js.map +1 -0
  33. package/dist/{command-TgbzPizb.js → command-D_kEnYcX.js} +17 -16
  34. package/dist/command-D_kEnYcX.js.map +1 -0
  35. package/dist/commands-7tH3EsVu.js +666 -0
  36. package/dist/commands-7tH3EsVu.js.map +1 -0
  37. package/dist/{commands-CthqwkAn.js → commands-BKGzBMgb.js} +1 -1
  38. package/dist/{config-DrVSEVN0.js → config-D7TFvUpL.js} +40 -55
  39. package/dist/config-D7TFvUpL.js.map +1 -0
  40. package/dist/{context-C1mxXh80.js → context-Dgn48BqT.js} +13 -2
  41. package/dist/context-Dgn48BqT.js.map +1 -0
  42. package/dist/{contrasts-5zsj32gk.js → contrasts-B4nOhn0Z.js} +3 -3
  43. package/dist/{contrasts-5zsj32gk.js.map → contrasts-B4nOhn0Z.js.map} +1 -1
  44. package/dist/db-DJUuFoWQ.js +602 -0
  45. package/dist/db-DJUuFoWQ.js.map +1 -0
  46. package/dist/db-W9EFlXnn.js +2 -0
  47. package/dist/{dist-CuY4ehEk.js → dist-Dp_AJgYN.js} +3 -3
  48. package/dist/{dist-CuY4ehEk.js.map → dist-Dp_AJgYN.js.map} +1 -1
  49. package/dist/{docker-D1HZDS6q.js → docker-CQxucRPe.js} +16 -15
  50. package/dist/docker-CQxucRPe.js.map +1 -0
  51. package/dist/{docker-DNmMPOBb.js → docker-Dac6Zalz.js} +1 -1
  52. package/dist/{docker-DOvjji9I.js → docker-DuHcQidD.js} +3 -3
  53. package/dist/{docker-DOvjji9I.js.map → docker-DuHcQidD.js.map} +1 -1
  54. package/dist/{doctor-D2MbNO8L.js → doctor-De_QlOTM.js} +72 -38
  55. package/dist/doctor-De_QlOTM.js.map +1 -0
  56. package/dist/{egress-2-qzPvGi.js → egress-DEAvq8Fl.js} +453 -31
  57. package/dist/egress-DEAvq8Fl.js.map +1 -0
  58. package/dist/env-BQqj93gu.js +21 -0
  59. package/dist/env-BQqj93gu.js.map +1 -0
  60. package/dist/{environment-Cca6a4wB.js → environment-B4IuURK1.js} +247 -14
  61. package/dist/environment-B4IuURK1.js.map +1 -0
  62. package/dist/{evaluate-DMyaGQEj.d.ts → evaluate-C9-ritA5.d.ts} +2 -2
  63. package/dist/{evaluate-DKIl54Hb.js → evaluate-Djk1YPFL.js} +540 -560
  64. package/dist/evaluate-Djk1YPFL.js.map +1 -0
  65. package/dist/facts-Cfx1H70y.js +898 -0
  66. package/dist/facts-Cfx1H70y.js.map +1 -0
  67. package/dist/files-Bs6H3HiG.js +65 -0
  68. package/dist/files-Bs6H3HiG.js.map +1 -0
  69. package/dist/{files-BpUYBwGu.js → files-CN1QfenC.js} +31 -8
  70. package/dist/files-CN1QfenC.js.map +1 -0
  71. package/dist/files-CtjEXWXm.js +1 -0
  72. package/dist/{files-BXHZFWmx.js → files-D27nUk0K.js} +6 -17
  73. package/dist/files-D27nUk0K.js.map +1 -0
  74. package/dist/files-D3gJa91B.js +1 -0
  75. package/dist/generate-BnrvBAzF.js +1646 -0
  76. package/dist/generate-BnrvBAzF.js.map +1 -0
  77. package/dist/{git-Crl3b87o.js → git-rPfl7AD1.js} +3 -3
  78. package/dist/{git-Crl3b87o.js.map → git-rPfl7AD1.js.map} +1 -1
  79. package/dist/{harness-CEuntrla.js → harness-1OkumOAm.js} +3 -3
  80. package/dist/{harness-CEuntrla.js.map → harness-1OkumOAm.js.map} +1 -1
  81. package/dist/harness.d.ts +1 -1
  82. package/dist/harness.js +1 -1
  83. package/dist/{header-DHQv_ZrF.js → header-BVzbRnNv.js} +10 -3
  84. package/dist/{header-DHQv_ZrF.js.map → header-BVzbRnNv.js.map} +1 -1
  85. package/dist/{images-Bi3aGxb9.js → images-m784ueGK.js} +61 -13
  86. package/dist/images-m784ueGK.js.map +1 -0
  87. package/dist/index.d.ts +3 -3
  88. package/dist/index.js +4 -4
  89. package/dist/init-hiTkiM4L.js +543 -0
  90. package/dist/init-hiTkiM4L.js.map +1 -0
  91. package/dist/init-kMkTgTZc.js +1 -0
  92. package/dist/insights-B2Xmhz9o.js +64 -0
  93. package/dist/insights-B2Xmhz9o.js.map +1 -0
  94. package/dist/{invocations-BYDPe3fV.js → invocations-D_ePcwyL.js} +4 -4
  95. package/dist/{invocations-BYDPe3fV.js.map → invocations-D_ePcwyL.js.map} +1 -1
  96. package/dist/{judge-DW_GDOmB.js → judge-CEDIfRiU.js} +50 -15
  97. package/dist/judge-CEDIfRiU.js.map +1 -0
  98. package/dist/judge.d.ts +7 -6
  99. package/dist/judge.js +8 -7
  100. package/dist/judge.js.map +1 -1
  101. package/dist/{live-jlfszAbb.js → live-DYd2QEyR.js} +2 -2
  102. package/dist/{live-jlfszAbb.js.map → live-DYd2QEyR.js.map} +1 -1
  103. package/dist/model-B8H3gL69.js +1 -0
  104. package/dist/{model-DaVCY3rb.js → model-CTiuzkWw.js} +9 -7
  105. package/dist/model-CTiuzkWw.js.map +1 -0
  106. package/dist/model-DcIbmggK.js +1 -0
  107. package/dist/{model-D0IcOQao.js → model-DpkclE4r.js} +60 -25
  108. package/dist/model-DpkclE4r.js.map +1 -0
  109. package/dist/{model-DaX4bNLZ.js → model-UgHDMhI1.js} +144 -46
  110. package/dist/model-UgHDMhI1.js.map +1 -0
  111. package/dist/{node-B3ydHo6t.js → node-CGZYh4Fj.js} +4 -4
  112. package/dist/{node-B3ydHo6t.js.map → node-CGZYh4Fj.js.map} +1 -1
  113. package/dist/{opencode-providers-J9colP0-.js → opencode-providers-snfeWd6Z.js} +86 -47
  114. package/dist/opencode-providers-snfeWd6Z.js.map +1 -0
  115. package/dist/{options-C0d0AzeR.js → options-CyAyLjgJ.js} +22 -11
  116. package/dist/options-CyAyLjgJ.js.map +1 -0
  117. package/dist/{pairs-CnInRu-G.js → pairs-DP6XOJby.js} +2 -2
  118. package/dist/{pairs-CnInRu-G.js.map → pairs-DP6XOJby.js.map} +1 -1
  119. package/dist/{patch-B1bnfhN-.js → patch-uMq9rCl_.js} +2 -2
  120. package/dist/{patch-B1bnfhN-.js.map → patch-uMq9rCl_.js.map} +1 -1
  121. package/dist/{plan-DgQxp9yy.js → plan-CZYgXnZh.js} +21 -8
  122. package/dist/plan-CZYgXnZh.js.map +1 -0
  123. package/dist/plugins-P-uh54VF.js +654 -0
  124. package/dist/plugins-P-uh54VF.js.map +1 -0
  125. package/dist/{preflight-DlQvbySb.js → preflight-BQlPTHVu.js} +1 -1
  126. package/dist/{preflight-BRzD30L5.js → preflight-D3HvKeAx.js} +14 -10
  127. package/dist/preflight-D3HvKeAx.js.map +1 -0
  128. package/dist/prepare-DCwM2KKD.js +2 -0
  129. package/dist/{prepare-B_y-AFMv.js → prepare-Dx7dOnJB.js} +70 -23
  130. package/dist/prepare-Dx7dOnJB.js.map +1 -0
  131. package/dist/{provenance-DQ0AXn6c.js → provenance-C_-8y-x3.js} +4 -4
  132. package/dist/{provenance-DQ0AXn6c.js.map → provenance-C_-8y-x3.js.map} +1 -1
  133. package/dist/{providers-DoTq1jHW.js → providers-CF5UEaP8.js} +24 -10
  134. package/dist/providers-CF5UEaP8.js.map +1 -0
  135. package/dist/providers-CTBYvMxu.js +1 -0
  136. package/dist/{query-BO-ux7RB.js → query-DXthKmq0.js} +3 -3
  137. package/dist/{query-BO-ux7RB.js.map → query-DXthKmq0.js.map} +1 -1
  138. package/dist/query-D_1ARx5q.js +1 -0
  139. package/dist/{questions-C2dym2ce.js → questions-KENpWnfV.js} +8 -9
  140. package/dist/questions-KENpWnfV.js.map +1 -0
  141. package/dist/{record-BKmMvZ6O.js → record-kk9M1hj-.js} +8 -4
  142. package/dist/record-kk9M1hj-.js.map +1 -0
  143. package/dist/{redact-DJ9JCShu.js → redact-CRGdfV2s.js} +4 -2
  144. package/dist/redact-CRGdfV2s.js.map +1 -0
  145. package/dist/remarks-1521zwfW.js +38 -0
  146. package/dist/remarks-1521zwfW.js.map +1 -0
  147. package/dist/remarks-BhAV1H4V.js +1 -0
  148. package/dist/remarks-Ml6bcSdV.js +278 -0
  149. package/dist/remarks-Ml6bcSdV.js.map +1 -0
  150. package/dist/{remote-CPPW86Q9.js → remote-Dkeghk8k.js} +3 -3
  151. package/dist/{remote-CPPW86Q9.js.map → remote-Dkeghk8k.js.map} +1 -1
  152. package/dist/resolve-CMmhACr6.js +1 -0
  153. package/dist/resolve-DzdWcIL6.js +724 -0
  154. package/dist/resolve-DzdWcIL6.js.map +1 -0
  155. package/dist/{results-BE2f-1XS.js → results-D-naai_Q.js} +33 -8
  156. package/dist/results-D-naai_Q.js.map +1 -0
  157. package/dist/{resume-Ca1c8vix.js → resume-8-nnW8iO.js} +4 -4
  158. package/dist/{resume-Ca1c8vix.js.map → resume-8-nnW8iO.js.map} +1 -1
  159. package/dist/{root-DICDHCtp.js → root-DNuLhaA5.js} +2 -2
  160. package/dist/root-DNuLhaA5.js.map +1 -0
  161. package/dist/rules-CcV3zLDR.js +318 -0
  162. package/dist/rules-CcV3zLDR.js.map +1 -0
  163. package/dist/run-Bel7Vde4.js +88 -0
  164. package/dist/run-Bel7Vde4.js.map +1 -0
  165. package/dist/{sandbox-DLrsvu7U.js → sandbox-YLbk9mO7.js} +2 -2
  166. package/dist/{sandbox-DLrsvu7U.js.map → sandbox-YLbk9mO7.js.map} +1 -1
  167. package/dist/sandbox.d.ts +1 -1
  168. package/dist/sandbox.js +1 -1
  169. package/dist/server-C-ysqkLv.js +1 -0
  170. package/dist/{server-TUY5xA_A.js → server-De8oH3bt.js} +10 -10
  171. package/dist/{server-TUY5xA_A.js.map → server-De8oH3bt.js.map} +1 -1
  172. package/dist/{source-DJGS0a0u.js → source-C686eZpY.js} +2 -2
  173. package/dist/{source-DJGS0a0u.js.map → source-C686eZpY.js.map} +1 -1
  174. package/dist/source-DjgypUHW.js +1 -0
  175. package/dist/src-3mP821Pt.js +2 -0
  176. package/dist/src-B3KCLZVk.js +3 -0
  177. package/dist/src-BAyg6CtQ.js +2 -0
  178. package/dist/{src-CF17Ug08.js → src-BYLM3hEz.js} +22 -6
  179. package/dist/src-BYLM3hEz.js.map +1 -0
  180. package/dist/src-BezgGX4X.js +2 -0
  181. package/dist/{src-DvCg0NBs.js → src-C7W6kg81.js} +40 -10
  182. package/dist/{src-DvCg0NBs.js.map → src-C7W6kg81.js.map} +1 -1
  183. package/dist/{src-ftu5wPSt.js → src-CIpKL45U.js} +782 -169
  184. package/dist/src-CIpKL45U.js.map +1 -0
  185. package/dist/src-ChBhk06Q.js +2 -0
  186. package/dist/{src-BxfCKA3_.js → src-CoqZYspK.js} +1 -1
  187. package/dist/{src-CFp86GtV.js → src-CzQrAcF8.js} +49 -49
  188. package/dist/src-CzQrAcF8.js.map +1 -0
  189. package/dist/src-D9fSTxlc.js +1 -0
  190. package/dist/{src-FGXhkEkH.js → src-DeY52Gyf.js} +182 -23
  191. package/dist/src-DeY52Gyf.js.map +1 -0
  192. package/dist/src-DrJSnoMR.js +2 -0
  193. package/dist/{src-DvK0EIZx.js → src-Xj_BZKbq.js} +112 -13
  194. package/dist/src-Xj_BZKbq.js.map +1 -0
  195. package/dist/{src-u8_ixn_F.js → src-cbCb-MPy.js} +20 -4
  196. package/dist/src-cbCb-MPy.js.map +1 -0
  197. package/dist/src-dhSTRN50.js +2 -0
  198. package/dist/{src-C7_1SqDP.js → src-djNNYIdP.js} +2 -2
  199. package/dist/{src-C7_1SqDP.js.map → src-djNNYIdP.js.map} +1 -1
  200. package/dist/state-BGWGAwdo.js +1 -0
  201. package/dist/{state-DfpHox6b.js → state-C0i_ujQZ.js} +19 -17
  202. package/dist/{state-DfpHox6b.js.map → state-C0i_ujQZ.js.map} +1 -1
  203. package/dist/studio-app/assets/{index-CBkhNAv8.js → index-Bb_cid9k.js} +9 -9
  204. package/dist/studio-app/index.html +1 -1
  205. package/dist/{suite-Cw_7G2RM.js → suite-DmuF_ApO.js} +80 -30
  206. package/dist/suite-DmuF_ApO.js.map +1 -0
  207. package/dist/suite-mn9g4veR.js +2 -0
  208. package/dist/{summary-BUKfU-9l.js → summary-BFMHMTz2.js} +273 -47
  209. package/dist/summary-BFMHMTz2.js.map +1 -0
  210. package/dist/testing.d.ts +2 -2
  211. package/dist/testing.js +2 -2
  212. package/dist/{text-gEcKjAyp.js → text-CrVy5ioL.js} +18 -2
  213. package/dist/text-CrVy5ioL.js.map +1 -0
  214. package/dist/{transcript-BNNANU8A.js → transcript-BTxJpW4R.js} +2 -2
  215. package/dist/{transcript-BNNANU8A.js.map → transcript-BTxJpW4R.js.map} +1 -1
  216. package/dist/{types-BYdlwMwo.d.ts → types-B-9b1MDV.d.ts} +850 -34
  217. package/dist/types-C5vI3g1C.js.map +1 -1
  218. package/dist/usage-BDo2q_yu.js +1 -0
  219. package/dist/{command-Bj39y-Wm.js → validate-DWjkshl1.js} +20 -377
  220. package/dist/validate-DWjkshl1.js.map +1 -0
  221. package/dist/view/index.d.ts +31 -263
  222. package/dist/view/index.js +8 -7
  223. package/dist/views-DE2A1jEX.js +2 -0
  224. package/dist/{views-B8w2J-gD.js → views-Dk8MCQKr.js} +8 -98
  225. package/dist/views-Dk8MCQKr.js.map +1 -0
  226. package/dist/{worker-entry-DNnveLqx.js → worker-entry-CqrzGPF4.js} +2 -2
  227. package/dist/{worker-entry-DNnveLqx.js.map → worker-entry-CqrzGPF4.js.map} +1 -1
  228. package/dist/workspace-B1eGO46M.js +1 -0
  229. package/dist/{workspace-C4RA3srV.js → workspace-CLeOybkr.js} +45 -11
  230. package/dist/workspace-CLeOybkr.js.map +1 -0
  231. package/package.json +5 -1
  232. package/skills/eval-rubric/SKILL.md +130 -0
  233. package/dist/agent-D483ru7D.js +0 -334
  234. package/dist/agent-D483ru7D.js.map +0 -1
  235. package/dist/assemble-D5KGrwnJ.js +0 -1
  236. package/dist/assemble-D9GWr-HS.js.map +0 -1
  237. package/dist/capture-CYGLpWxR.js +0 -1
  238. package/dist/cli-ClpPT8kn.js.map +0 -1
  239. package/dist/cli-spec-qmEnQ25_.js.map +0 -1
  240. package/dist/command-Bj39y-Wm.js.map +0 -1
  241. package/dist/command-C-cVjlny.js +0 -1
  242. package/dist/command-DbAe1Rzb.js.map +0 -1
  243. package/dist/command-TgbzPizb.js.map +0 -1
  244. package/dist/commands-YUwlyTbr.js +0 -228
  245. package/dist/commands-YUwlyTbr.js.map +0 -1
  246. package/dist/config-DrVSEVN0.js.map +0 -1
  247. package/dist/context-C1mxXh80.js.map +0 -1
  248. package/dist/db-CKg5NtiW.js +0 -2
  249. package/dist/db-DfhipHEF.js +0 -1416
  250. package/dist/db-DfhipHEF.js.map +0 -1
  251. package/dist/docker-D1HZDS6q.js.map +0 -1
  252. package/dist/doctor-D2MbNO8L.js.map +0 -1
  253. package/dist/egress-2-qzPvGi.js.map +0 -1
  254. package/dist/environment-Cca6a4wB.js.map +0 -1
  255. package/dist/evaluate-DKIl54Hb.js.map +0 -1
  256. package/dist/files-BXHZFWmx.js.map +0 -1
  257. package/dist/files-BpUYBwGu.js.map +0 -1
  258. package/dist/files-Bw8lWEjg.js +0 -1
  259. package/dist/files-D6J0iSdx.js +0 -1
  260. package/dist/images-Bi3aGxb9.js.map +0 -1
  261. package/dist/init-Cl3f3NBT.js +0 -360
  262. package/dist/init-Cl3f3NBT.js.map +0 -1
  263. package/dist/init-fhOXED8F.js +0 -1
  264. package/dist/judge-DW_GDOmB.js.map +0 -1
  265. package/dist/model-CXhrziBe.js +0 -1
  266. package/dist/model-D0IcOQao.js.map +0 -1
  267. package/dist/model-DaVCY3rb.js.map +0 -1
  268. package/dist/model-DaX4bNLZ.js.map +0 -1
  269. package/dist/model-_bxNrk2h.js +0 -1
  270. package/dist/opencode-providers-J9colP0-.js.map +0 -1
  271. package/dist/options-C0d0AzeR.js.map +0 -1
  272. package/dist/plan-DgQxp9yy.js.map +0 -1
  273. package/dist/preflight-BRzD30L5.js.map +0 -1
  274. package/dist/prepare-B_y-AFMv.js.map +0 -1
  275. package/dist/prepare-DCKTLHN3.js +0 -2
  276. package/dist/providers-B9yTdIqF.js +0 -1
  277. package/dist/providers-DoTq1jHW.js.map +0 -1
  278. package/dist/query-CRQfsw0e.js +0 -1
  279. package/dist/questions-C2dym2ce.js.map +0 -1
  280. package/dist/record-BKmMvZ6O.js.map +0 -1
  281. package/dist/redact-DJ9JCShu.js.map +0 -1
  282. package/dist/results-BE2f-1XS.js.map +0 -1
  283. package/dist/root-DICDHCtp.js.map +0 -1
  284. package/dist/server-Dp8eOeK5.js +0 -1
  285. package/dist/source-DdbLGR1M.js +0 -1
  286. package/dist/src-CF17Ug08.js.map +0 -1
  287. package/dist/src-CFp86GtV.js.map +0 -1
  288. package/dist/src-CLoOMVWI.js +0 -3
  289. package/dist/src-CcrQbpG8.js +0 -2
  290. package/dist/src-DCFtJRp0.js +0 -2
  291. package/dist/src-DSYYPEOj.js +0 -2
  292. package/dist/src-DYO3DdnQ.js +0 -1
  293. package/dist/src-DvK0EIZx.js.map +0 -1
  294. package/dist/src-FGXhkEkH.js.map +0 -1
  295. package/dist/src-WD-zgQYZ.js +0 -2
  296. package/dist/src-_oI9AePb.js +0 -2
  297. package/dist/src-ftu5wPSt.js.map +0 -1
  298. package/dist/src-qsKCqv7a.js +0 -2
  299. package/dist/src-u8_ixn_F.js.map +0 -1
  300. package/dist/state-DpAxYyED.js +0 -1
  301. package/dist/suite-Cw_7G2RM.js.map +0 -1
  302. package/dist/suite-DamAEnNF.js +0 -1
  303. package/dist/summary-BUKfU-9l.js.map +0 -1
  304. package/dist/summary-Baa4rTHS.js +0 -192
  305. package/dist/summary-Baa4rTHS.js.map +0 -1
  306. package/dist/text-gEcKjAyp.js.map +0 -1
  307. package/dist/tools-DLHgetpx.js +0 -249
  308. package/dist/tools-DLHgetpx.js.map +0 -1
  309. package/dist/views-B8w2J-gD.js.map +0 -1
  310. package/dist/views-DblnI3kA.js +0 -2
  311. package/dist/workspace-C4RA3srV.js.map +0 -1
  312. package/dist/workspace-C_X0tdv6.js +0 -1
@@ -0,0 +1,1646 @@
1
+ import { l as sha256 } from "./ids-Bp6ut7ZD.js";
2
+ import { l as ownFile } from "./root-DNuLhaA5.js";
3
+ import { a as oneLine, i as formatDuration, n as clip } from "./text-CrVy5ioL.js";
4
+ import { i as cannedModel, o as providerModel } from "./model-DpkclE4r.js";
5
+ import { C as parseTranscript } from "./model-UgHDMhI1.js";
6
+ import { c as suiteResult, r as countedOf, t as cancelledOf } from "./suite-DmuF_ApO.js";
7
+ import { i as REMARK_KINDS, n as MODEL_REMARK_KINDS } from "./insights-B2Xmhz9o.js";
8
+ import { g as slotsOf, y as factsHash } from "./summary-BFMHMTz2.js";
9
+ import { n as readInsights, t as insightsFile } from "./files-Bs6H3HiG.js";
10
+ import { t as unverifiedNumbers } from "./validate-DWjkshl1.js";
11
+ import { t as analysisCacheDir } from "./files-D27nUk0K.js";
12
+ import { r as quotedRemarkId } from "./rules-CcV3zLDR.js";
13
+ import { readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { z } from "zod";
16
+ import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
17
+ import { NoObjectGeneratedError, Output, generateText } from "ai";
18
+ //#region src/insights/classify.ts
19
+ const classifyDir = (root) => join(analysisCacheDir(root), "classify");
20
+ const RUN_FILE = /^\d{8}T\d{6}Z_.+\.json$/;
21
+ /** The intents of the latest recorded run that has insights, by classification key. */
22
+ async function readBack(historyDir) {
23
+ const runs = (await readdir(historyDir).catch(() => [])).filter((n) => RUN_FILE.test(n)).map((n) => n.slice(0, -5)).sort().reverse();
24
+ for (const runId of runs) {
25
+ const doc = await readInsights(insightsFile(historyDir, runId)).catch(() => null);
26
+ if (!doc) continue;
27
+ return new Map(doc.evals.map(({ intent: { key, job, question, subject, name } }) => [key, {
28
+ job,
29
+ question,
30
+ subject,
31
+ name
32
+ }]));
33
+ }
34
+ return /* @__PURE__ */ new Map();
35
+ }
36
+ /** The cache for a project: its local files first, then the latest recorded run's insights. */
37
+ function classifyCache(root, historyDir) {
38
+ const dir = classifyDir(root);
39
+ let recorded;
40
+ return {
41
+ async get(key) {
42
+ const text = await readFile(join(dir, `${key}.json`), "utf8").catch(() => null);
43
+ if (text !== null) try {
44
+ const value = JSON.parse(text);
45
+ if (value && typeof value.job === "string" && typeof value.question === "string") return value;
46
+ } catch {}
47
+ recorded ??= readBack(historyDir);
48
+ return (await recorded).get(key) ?? null;
49
+ },
50
+ async set(key, value) {
51
+ await mkdir(dir, { recursive: true });
52
+ const path = join(dir, `${key}.json`);
53
+ const tmp = `${path}.${process.pid}.tmp`;
54
+ await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`);
55
+ await rename(tmp, path);
56
+ }
57
+ };
58
+ }
59
+ //#endregion
60
+ //#region src/insights/budget.ts
61
+ /** What an eval is estimated to use before any eval has finished, from gemini-3.8-flash runs. */
62
+ const FIRST_ESTIMATE = 6e3;
63
+ const ledger = (maxTokens) => ({
64
+ maxTokens,
65
+ used: 0,
66
+ reserved: 0,
67
+ finished: 0
68
+ });
69
+ /** What the next eval reserves: the finished evals' mean, else `FIRST_ESTIMATE`. */
70
+ const estimate = (l) => l.finished ? Math.ceil(l.used / l.finished) : FIRST_ESTIMATE;
71
+ /** Whether the next eval in priority order starts now, waits for one in flight, or is skipped. */
72
+ function admit(l) {
73
+ if (l.used + l.reserved + estimate(l) <= l.maxTokens) return "start";
74
+ return l.reserved > 0 ? "wait" : "skip";
75
+ }
76
+ /** Reserves the next eval's estimate; returns it, to hand back to `settle`. */
77
+ function reserve(l) {
78
+ const tokens = estimate(l);
79
+ l.reserved += tokens;
80
+ return tokens;
81
+ }
82
+ /**
83
+ * Swaps an eval's reservation for what it used. One that used nothing (its calls failed or timed
84
+ * out before any usage came back) says nothing of what the next will use, so it leaves the mean.
85
+ */
86
+ function settle(l, reserved, used) {
87
+ l.reserved -= reserved;
88
+ l.used += used;
89
+ if (used > 0) l.finished++;
90
+ }
91
+ //#endregion
92
+ //#region src/insights/input.ts
93
+ /** The words an effect prints as, and the only ones a sentence may use for it. */
94
+ const EFFECT_WORDS = {
95
+ better: "helped",
96
+ worse: "made it worse",
97
+ same: "no difference",
98
+ unclear: "can't tell yet"
99
+ };
100
+ const TEXT_CHARS = 1500;
101
+ const LINE_CHARS = 200;
102
+ const verdictOf = (o) => o === "infra_error" ? "infra error" : o;
103
+ const armOf = (c) => c.variant ?? (c.environment === "-" ? "baseline" : c.environment);
104
+ function checkFact(k, measured) {
105
+ const o = k.outcome;
106
+ if (o.status === "not run") return {
107
+ path: k.path,
108
+ kind: "check",
109
+ label: k.label,
110
+ result: "not run"
111
+ };
112
+ const answered = o.passed + o.failed + o.errored;
113
+ return {
114
+ path: k.path,
115
+ kind: "check",
116
+ label: k.label,
117
+ result: `${o.passed} of ${answered} passed`,
118
+ ...o.notRun ? { notRun: o.notRun } : {},
119
+ ...o.errored ? { couldNotCheck: o.errored } : {},
120
+ ...o.assertion ? { assertion: clip(o.assertion.trim().split("\n")[0] ?? "", 240) } : {},
121
+ ...k.effect && measured ? { effect: EFFECT_WORDS[k.effect] } : {},
122
+ ...k.judge ? { judge: true } : {}
123
+ };
124
+ }
125
+ /**
126
+ * A cell's facts. Its tries leave out the attempts the run's interrupt cancelled, as its row
127
+ * does; under a cancelled eval or case (`measured` false) no effect is given, so none is claimed.
128
+ */
129
+ function cellFacts(c, measured) {
130
+ const cancelled = cancelledOf(c);
131
+ const out = [{
132
+ path: c.path,
133
+ kind: "cell",
134
+ harness: c.harness,
135
+ arm: armOf(c),
136
+ sandbox: c.sandbox,
137
+ verdict: verdictOf(c.outcome),
138
+ passed: `${c.passed} of ${countedOf(c)} tries`,
139
+ ...c.infraError ? { infraErrors: c.infraError } : {},
140
+ ...cancelled ? { cancelled } : {},
141
+ ...c.baseline ? { baseline: true } : {},
142
+ ...c.effect && measured ? { effect: EFFECT_WORDS[c.effect] } : {},
143
+ ...c.context.length ? { context: c.context } : {},
144
+ ...c.errors.length ? { errors: c.errors.map((e) => `${e.kind}: ${clip(e.message, LINE_CHARS)}`) } : {}
145
+ }, ...c.checks.map((k) => checkFact(k, measured))];
146
+ if (c.effort) out.push({
147
+ path: `${c.path}/effort`,
148
+ kind: "effort",
149
+ toolCalls: c.effort.tools,
150
+ seconds: c.effort.agentMs === null ? null : Math.round(c.effort.agentMs / 100) / 10,
151
+ tokens: c.effort.tokens
152
+ });
153
+ return out;
154
+ }
155
+ function caseFact(k) {
156
+ const passed = k.cells.reduce((n, c) => n + c.passed, 0);
157
+ const counted = k.cells.reduce((n, c) => n + countedOf(c), 0);
158
+ return {
159
+ path: k.path,
160
+ kind: "case",
161
+ label: k.label,
162
+ verdict: verdictOf(k.outcome),
163
+ passed: `${passed} of ${counted} tries`
164
+ };
165
+ }
166
+ /** Every fact of the eval, its own node first. */
167
+ function evalFacts(e) {
168
+ const out = [{
169
+ path: e.path,
170
+ kind: e.family ? "family" : "eval",
171
+ name: e.name,
172
+ verdict: verdictOf(e.outcome),
173
+ ...e.cases ? { cases: e.cases.length } : {}
174
+ }];
175
+ const measured = e.outcome !== "cancelled";
176
+ for (const c of e.cells ?? []) out.push(...cellFacts(c, measured));
177
+ for (const k of e.cases ?? []) {
178
+ out.push(caseFact(k));
179
+ for (const c of k.cells) out.push(...cellFacts(c, measured && k.outcome !== "cancelled"));
180
+ }
181
+ for (const p of e.pooled ?? []) out.push({
182
+ path: p.path,
183
+ kind: "all cases",
184
+ arm: p.key,
185
+ ...measured ? { effect: EFFECT_WORDS[p.effect] } : {},
186
+ verdict: p.outcome,
187
+ pairs: p.pairs
188
+ });
189
+ return out;
190
+ }
191
+ /** The record's evals behind a result: the eval, or each of a family's cases, with their cells' paths. */
192
+ function entriesOf(record, e) {
193
+ const cells = (entry, results) => new Map(results.map((c) => [c.key, c]).filter(([k]) => entry.cells.some((x) => x.key === k)));
194
+ if (e.cases) return e.cases.flatMap((k) => {
195
+ const entry = record.evals.find((x) => x.id === k.id);
196
+ return entry ? [{
197
+ entry,
198
+ cells: cells(entry, k.cells)
199
+ }] : [];
200
+ });
201
+ const entry = record.evals.find((x) => x.id === e.id);
202
+ return entry ? [{
203
+ entry,
204
+ cells: cells(entry, e.cells ?? [])
205
+ }] : [];
206
+ }
207
+ /** An attempt's id in insights: its cell's path and its number. */
208
+ const attemptId = (cell, a) => `${cell.path}#${a.n}`;
209
+ /** A pass or a fail the agent made: not an infra error, a skip, or one the interrupt cancelled. */
210
+ const counted = (a) => a.outcome === "pass" || a.outcome === "fail" && a.error?.kind !== "aborted";
211
+ /** Each counted attempt's id and its whole final message, as the record keeps it. */
212
+ function attemptTexts(record, e) {
213
+ const out = /* @__PURE__ */ new Map();
214
+ for (const { entry, cells } of entriesOf(record, e)) for (const a of entry.attempts) {
215
+ const cell = cells.get(a.cell);
216
+ if (cell && counted(a)) out.set(attemptId(cell, a), a.turns.at(-1)?.text ?? "");
217
+ }
218
+ return out;
219
+ }
220
+ function describeEvent(line) {
221
+ switch (line.type) {
222
+ case "text": return `text: ${line.text}`;
223
+ case "tool": return `tool ${line.tool}${line.ok ? "" : " (failed)"}: ${JSON.stringify(line.input)}`;
224
+ case "web-search": return `web search: ${line.query}`;
225
+ case "web-fetch": return `web fetch: ${line.url}`;
226
+ case "error": return `error: ${line.message}`;
227
+ default: return null;
228
+ }
229
+ }
230
+ /** A failed attempt's last tool calls and messages, numbered by their line in transcript.jsonl. */
231
+ async function transcriptLines(runDir, a) {
232
+ if (a.outcome !== "fail" || !a.files.transcript) return [];
233
+ const text = await readFile(join(runDir, ...a.files.transcript.path.split("/")), "utf8").catch(() => null);
234
+ if (!text) return [];
235
+ const out = [];
236
+ text.split("\n").forEach((raw, i) => {
237
+ const [line] = raw.trim() ? parseTranscript(raw) : [];
238
+ const shown = line && describeEvent(line);
239
+ if (shown) out.push(`L${i + 1} ${clip(shown.replace(/\s+/g, " "), LINE_CHARS)}`);
240
+ });
241
+ return out.slice(-14);
242
+ }
243
+ async function attemptsOf(record, runDir, e) {
244
+ const out = [];
245
+ for (const { entry, cells } of entriesOf(record, e)) for (const a of entry.attempts) {
246
+ const cell = cells.get(a.cell);
247
+ if (!cell || !counted(a)) continue;
248
+ out.push({
249
+ id: attemptId(cell, a),
250
+ cell: cell.path,
251
+ outcome: a.outcome,
252
+ lastText: clip(a.turns.at(-1)?.text ?? "", TEXT_CHARS),
253
+ lines: runDir ? await transcriptLines(runDir, a) : []
254
+ });
255
+ }
256
+ return out;
257
+ }
258
+ const unique = (xs) => [...new Set(xs.filter(Boolean))];
259
+ /** The prompts, judge questions and check labels classification reads after the name. */
260
+ function declared(record, e) {
261
+ const entries = entriesOf(record, e).map((x) => x.entry);
262
+ return {
263
+ prompts: (entries.flatMap((x) => x.attempts).filter((a) => a.turns.length).sort((a, b) => (a.seq ?? Infinity) - (b.seq ?? Infinity))[0]?.turns ?? []).map((t) => clip(t.prompt, 1e3)),
264
+ judgeQuestions: unique(entries.flatMap((x) => x.attempts.flatMap((a) => a.judgements.map((j) => j.question)))),
265
+ checks: unique(entries.flatMap((x) => x.cells.flatMap((c) => (c.checks ?? []).map((k) => k.label)))),
266
+ sources: entries[0]?.sources ?? []
267
+ };
268
+ }
269
+ /** The eval's input to both of its calls; `call` and the insights call's fields are set by the caller. */
270
+ async function evalInput(record, runDir, e) {
271
+ const { prompts, judgeQuestions, checks } = declared(record, e);
272
+ return {
273
+ eval: {
274
+ path: e.path,
275
+ name: e.template ?? e.name,
276
+ group: e.group,
277
+ file: e.id.split(" › ")[0] ?? "",
278
+ across: e.across,
279
+ family: e.family,
280
+ verdict: verdictOf(e.outcome)
281
+ },
282
+ prompts,
283
+ judgeQuestions,
284
+ checks,
285
+ facts: evalFacts(e),
286
+ attempts: await attemptsOf(record, runDir, e)
287
+ };
288
+ }
289
+ /** The most source lines an eval's input shows: its declaration, from its `evaluate` on. */
290
+ const SOURCE_LINES = 80;
291
+ /** A line that starts another eval in the same file. */
292
+ const NEXT_EVAL = /\bevaluate(?:\.each|\.todo|\.skip|\.only)?\s*[(<]/;
293
+ /**
294
+ * Where the eval named `name` is declared in a file's lines: from the `evaluate` that names it (or
295
+ * a family's template) up to the next eval's, at most `SOURCE_LINES`, as 0-based `[from, to)`.
296
+ * Null when no line names it.
297
+ */
298
+ function declarationOf(lines, name) {
299
+ const named = lines.findIndex((l) => [
300
+ `'${name}'`,
301
+ `"${name}"`,
302
+ `\`${name}\``
303
+ ].some((q) => l.includes(q)));
304
+ if (named < 0) return null;
305
+ let from = named;
306
+ for (let i = named; i >= Math.max(0, named - 10); i--) if (NEXT_EVAL.test(lines[i] ?? "")) {
307
+ from = i;
308
+ break;
309
+ }
310
+ let to = Math.min(lines.length, from + SOURCE_LINES);
311
+ for (let i = named + 1; i < to; i++) if (NEXT_EVAL.test(lines[i] ?? "")) {
312
+ to = i;
313
+ break;
314
+ }
315
+ return {
316
+ from,
317
+ to
318
+ };
319
+ }
320
+ /**
321
+ * The eval's declaration in its file, numbered as the file is (`declarationOf`), or the file's
322
+ * first `SOURCE_LINES` when no line names it. Null when the file can't be read.
323
+ */
324
+ async function evalSource(root, record, e) {
325
+ const entry = entriesOf(record, e)[0]?.entry;
326
+ if (!entry) return null;
327
+ const text = await readFile(join(root, entry.file), "utf8").catch(() => null);
328
+ if (text === null) return null;
329
+ const lines = text.split("\n");
330
+ const { from, to } = declarationOf(lines, entry.family?.name ?? entry.name) ?? {
331
+ from: 0,
332
+ to: Math.min(lines.length, SOURCE_LINES)
333
+ };
334
+ return {
335
+ file: entry.file,
336
+ lines: lines.slice(from, to).map((l, i) => `L${from + i + 1} ${clip(l, LINE_CHARS)}`.trimEnd())
337
+ };
338
+ }
339
+ /** The classification's cache key: what it read, the model and the prompt version. */
340
+ function classificationKey(record, e, model, promptVersion) {
341
+ const { prompts, judgeQuestions, checks, sources } = declared(record, e);
342
+ return sha256(JSON.stringify({
343
+ id: e.id,
344
+ sources,
345
+ prompts,
346
+ judgeQuestions,
347
+ checks,
348
+ model,
349
+ promptVersion
350
+ }));
351
+ }
352
+ const TRANSCRIPT_ID = /^(A\d+)#L(\d+)$/;
353
+ const ID = String.raw`\b[FA]\d+(?:#L\d+)?\b`;
354
+ const ID_LIST = new RegExp(String.raw`\s*[([]${ID}(?:\s*(?:,|;|and)\s*${ID})*[)\]]`, "g");
355
+ /** A short id left in a sentence's text, which the reader would see. */
356
+ const ID_IN_TEXT = new RegExp(ID);
357
+ /** The text without the bracketed id lists models append as citations (`… each (F2, F7).`). */
358
+ const withoutIdLists = (text) => text.replace(ID_LIST, "");
359
+ /**
360
+ * The eval's input with a short id for each fact (`F1`…) and attempt (`A1`…), and the map back:
361
+ * the model cites ids, not the long percent-encoded paths, and the validator sees paths again.
362
+ */
363
+ function modelInput(input) {
364
+ const factIds = new Map(input.facts.map((f, i) => [f.path, `F${i + 1}`]));
365
+ const attemptIds = new Map(input.attempts.map((a, i) => [a.id, `A${i + 1}`]));
366
+ const facts = new Map([...factIds].map(([path, id]) => [id, path]));
367
+ const attempts = new Map([...attemptIds].map(([path, id]) => [id, path]));
368
+ const view = {
369
+ ...input,
370
+ facts: input.facts.map((f) => ({
371
+ id: factIds.get(f.path),
372
+ ...f
373
+ })),
374
+ attempts: input.attempts.map((a) => ({
375
+ ...a,
376
+ id: attemptIds.get(a.id),
377
+ cell: factIds.get(a.cell) ?? a.cell
378
+ })),
379
+ ...input.slots ? { slots: {
380
+ ...input.slots,
381
+ rows: input.slots.rows.map((r) => factIds.get(r) ?? r)
382
+ } } : {}
383
+ };
384
+ const cite = (c) => {
385
+ const t = TRANSCRIPT_ID.exec(c.trim());
386
+ if (t) {
387
+ const attempt = attempts.get(t[1]);
388
+ return attempt ? `transcript:${attempt}#L${t[2]}` : c;
389
+ }
390
+ return facts.get(c.trim()) ?? c;
391
+ };
392
+ return {
393
+ input: view,
394
+ ids: {
395
+ cite,
396
+ row: (r) => facts.get(r.trim()) ?? r,
397
+ attempt: (a) => attempts.get(a.trim()) ?? a
398
+ }
399
+ };
400
+ }
401
+ /** The headline call's input with a short id for each eval, and the map back to their paths. */
402
+ function modelHeadlineInput(input) {
403
+ const paths = new Map(input.evals.map((e, i) => [`F${i + 1}`, e.path]));
404
+ const cite = (c) => paths.get(c.trim()) ?? c;
405
+ return {
406
+ input: {
407
+ ...input,
408
+ evals: input.evals.map((e, i) => ({
409
+ id: `F${i + 1}`,
410
+ ...e
411
+ }))
412
+ },
413
+ ids: {
414
+ cite,
415
+ row: cite,
416
+ attempt: (a) => a
417
+ }
418
+ };
419
+ }
420
+ //#endregion
421
+ //#region src/insights/priority.ts
422
+ const cellsOf = (e) => [...e.cells ?? [], ...(e.cases ?? []).flatMap((k) => k.cells)];
423
+ const madeWorse = (e) => (e.pooled ?? []).some((p) => p.effect === "worse") || cellsOf(e).some((c) => c.effect === "worse" || c.checks.some((k) => k.effect === "worse"));
424
+ /** 0 is spent first. */
425
+ function insightTier(e) {
426
+ if (e.outcome === "fail") return 0;
427
+ if (madeWorse(e)) return 1;
428
+ if (e.questions.length || e.across !== "none") return 2;
429
+ if (e.outcome === "infra_error") return 3;
430
+ if (e.outcome === "pass" && cellsOf(e).some((c) => c.failed > 0)) return 4;
431
+ return 5;
432
+ }
433
+ /** `evals` (in output order) in the order the budget reaches them. */
434
+ const byPriority = (evals) => evals.map((e, i) => ({
435
+ e,
436
+ i,
437
+ tier: insightTier(e)
438
+ })).sort((a, b) => a.tier - b.tier || a.i - b.i).map((x) => x.e);
439
+ /** The most findings an eval prints. */
440
+ const MAX_FINDINGS = 3;
441
+ /** Words the output never uses: the statistics behind a verdict, and reeval's own terms for cells. */
442
+ const BANNED = [
443
+ {
444
+ word: "within noise",
445
+ pattern: /\bwithin (the )?noise\b/i
446
+ },
447
+ {
448
+ word: "too close to call",
449
+ pattern: /\btoo close to call\b/i
450
+ },
451
+ {
452
+ word: "effect size",
453
+ pattern: /\beffect sizes?\b/i
454
+ },
455
+ {
456
+ word: "interval",
457
+ pattern: /\bintervals?\b/i
458
+ },
459
+ {
460
+ word: "significance",
461
+ pattern: /\b(statistical(ly)? )?significan(ce|t|tly)\b/i
462
+ },
463
+ {
464
+ word: "noise",
465
+ pattern: /\bnois(e|y)\b/i
466
+ },
467
+ {
468
+ word: "p-value",
469
+ pattern: /\bp-?values?\b|\bp\s*[<=>≤]\s*0?\.\d/i
470
+ },
471
+ {
472
+ word: "confidence",
473
+ pattern: /\bconfiden(ce|t)\b/i
474
+ },
475
+ {
476
+ word: "Newcombe",
477
+ pattern: /\bnewcombe\b/i
478
+ },
479
+ {
480
+ word: "Wilson",
481
+ pattern: /\bwilson\b/i
482
+ },
483
+ {
484
+ word: "pass@1",
485
+ pattern: /\bpass@1\b/i
486
+ },
487
+ {
488
+ word: "lift",
489
+ pattern: /\blifts?\b/i
490
+ },
491
+ {
492
+ word: "control",
493
+ pattern: /\bcontrols?\b/i
494
+ },
495
+ {
496
+ word: "variant",
497
+ pattern: /\bvariants?\b/i
498
+ },
499
+ {
500
+ word: "arm",
501
+ pattern: /\barms?\b/i
502
+ },
503
+ {
504
+ word: "cell",
505
+ pattern: /\bcells?\b/i
506
+ },
507
+ {
508
+ word: "discriminates",
509
+ pattern: /\bdiscriminat\w*/i
510
+ }
511
+ ];
512
+ const TRANSCRIPT = /^transcript:(.+)#L(\d+)$/;
513
+ /** A quoted span: double, curly or backtick quotes, or single quotes that aren't an apostrophe. */
514
+ const QUOTE = /["“”]([^"“”]+)["“”]|‘(.+?)’(?!\w)|`([^`]+)`|(?<![\w'])'([^'\n]+?)'(?!\w)/g;
515
+ const PERCENT = /\d\s*%|\bper ?cent\b/i;
516
+ /**
517
+ * Each effect's word and the claims that say it: the word itself and the plain words for it
518
+ * (`helps`, `improved`, `worse`, `hurt`). A negated `help` (`didn't help`) claims no `helped`.
519
+ */
520
+ const EFFECT_PHRASES = [
521
+ {
522
+ word: EFFECT_WORDS.better,
523
+ pattern: /(?<!(?:n['’]t|\bnot|\bnever)\s+)\b(?:help(?:s|ed|ing)?|improv(?:e|es|ed|ing))\b/gi
524
+ },
525
+ {
526
+ word: EFFECT_WORDS.worse,
527
+ pattern: /\b(?:(?:make|makes|made|making)\s+(?:it|things|them)\s+)?worse\b|\bhurt(?:s|ing)?\b/gi
528
+ },
529
+ {
530
+ word: EFFECT_WORDS.same,
531
+ pattern: /\bno difference\b/gi
532
+ },
533
+ {
534
+ word: EFFECT_WORDS.unclear,
535
+ pattern: /\bcan['’]t tell yet\b/gi
536
+ }
537
+ ];
538
+ /**
539
+ * A "can't tell yet if/whether …" clause, up to the end of its sentence, any comma, colon,
540
+ * semicolon, dash or bracket, a conjunction, or an "and" that starts a new clause. It errs short:
541
+ * a word past it is checked.
542
+ */
543
+ const HEDGED = /\bcan['’]t tell yet (if|whether)\b.*?(?=[.,:;!?()–—]|\s-\s|\s+(but|while|though|although|whereas|yet|so|because)\b|\s+and\s+(it|they|the|this|that|its|their|with|plain)\b|$)/gi;
544
+ const NUMBER_WORDS = [
545
+ "zero",
546
+ "one",
547
+ "two",
548
+ "three",
549
+ "four",
550
+ "five",
551
+ "six",
552
+ "seven",
553
+ "eight",
554
+ "nine",
555
+ "ten",
556
+ "eleven",
557
+ "twelve",
558
+ "thirteen",
559
+ "fourteen",
560
+ "fifteen",
561
+ "sixteen",
562
+ "seventeen",
563
+ "eighteen",
564
+ "nineteen",
565
+ "twenty"
566
+ ];
567
+ const COUNT = String.raw`(\d+|${NUMBER_WORDS.join("|")})`;
568
+ /** `N of M`, `N out of M` or `N of the M`, in digits or words. */
569
+ const PAIR = new RegExp(String.raw`(?<![\w.])${COUNT}\s+(?:out\s+)?of\s+(?:the\s+|its\s+|their\s+)?${COUNT}\b`, "gi");
570
+ const FACT_PAIR = /\b(\d+) of (\d+)\b/g;
571
+ const OUTCOME_VERB = /\b(?:pass|fail|succeed|miss)\w*/gi;
572
+ const PASSED_AFTER = /^\s+(?:tries\s+|checks\s+)?pass(?:ed|es|ing)?\b/i;
573
+ const SMALL = /(?<![\w.#/:-])\d(?![\w/]|\.\d)/g;
574
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
575
+ const countOf = (raw) => {
576
+ const word = NUMBER_WORDS.indexOf(raw.toLowerCase());
577
+ return word >= 0 ? word : Number(raw);
578
+ };
579
+ /** Where a count's clause starts: a clause mark, a dash, or the end of the count before it. */
580
+ const CLAUSE_END = /[.,:;!?()–—]|\s-\s/g;
581
+ /** The tool a count's clause names after its last outcome verb, lower-cased, or null. */
582
+ function toolBefore(clause, tools) {
583
+ const verbAt = [...clause.matchAll(OUTCOME_VERB)].at(-1)?.index ?? -1;
584
+ let found = null;
585
+ for (const tool of tools) {
586
+ const re = new RegExp(`(?<!\\w)${escapeRegExp(tool)}(?!\\w)`, "gi");
587
+ const at = [...clause.matchAll(re)].at(-1)?.index ?? -1;
588
+ if (at > verbAt && (!found || at > found.at)) found = {
589
+ tool,
590
+ at
591
+ };
592
+ }
593
+ return found ? found.tool.toLowerCase() : null;
594
+ }
595
+ /**
596
+ * The counts and numbers in `text` that what it cites doesn't hold. An `N of M` must be a cited
597
+ * fact's own count (a transcript line's text isn't a count). When the facts count passes and the
598
+ * nearest verb before it is a fail, it counts failures: `failed 4 of 4` over `0 of 4 passed`, and
599
+ * `failed 0 of 4` there is dropped, unless `passed` follows it. A count whose clause names a tool
600
+ * after its last outcome verb (`recommended Genkit in 1 of 1 tries`) counts the tries that named
601
+ * it, so it must be that tool's count over a cited fact, not a pass count. Any other number must
602
+ * be in what it cites, as `unverifiedNumbers` reads them, and a digit from 0 to 9 exactly.
603
+ */
604
+ function unheldNumbers(text, facts, all, passCounts, tools) {
605
+ const known = new Set(facts.flatMap((f) => [...f.matchAll(FACT_PAIR)].map((m) => m[0])));
606
+ const out = [];
607
+ let from = 0;
608
+ for (const m of text.matchAll(PAIR)) {
609
+ const at = m.index ?? 0;
610
+ const n = countOf(m[1]);
611
+ const of = countOf(m[2]);
612
+ const before = text.slice(0, at);
613
+ const passedAfter = PASSED_AFTER.test(text.slice(at + m[0].length));
614
+ const clauseAt = Math.max(from, ...[...before.matchAll(CLAUSE_END)].map((c) => c.index + 1));
615
+ from = at + m[0].length;
616
+ const tool = tools && !passedAfter ? toolBefore(before.slice(clauseAt), tools.names) : null;
617
+ if (tool !== null) {
618
+ if (!tools?.held(tool).has(`${n} of ${of}`)) out.push(m[0]);
619
+ continue;
620
+ }
621
+ const verb = [...before.matchAll(OUTCOME_VERB)].at(-1)?.[0] ?? "";
622
+ const failures = passCounts && /^(fail|miss)/i.test(verb) && !passedAfter;
623
+ if (known.has(failures ? `${of - n} of ${of}` : `${n} of ${of}`)) continue;
624
+ out.push(m[0]);
625
+ }
626
+ const rest = text.replace(PAIR, " ");
627
+ const digits = new Set(all.flatMap((t) => t.match(/\d+(?:\.\d+)?/g) ?? []));
628
+ const small = (rest.match(SMALL) ?? []).filter((d) => !digits.has(d));
629
+ return [.../* @__PURE__ */ new Set([
630
+ ...out,
631
+ ...small,
632
+ ...unverifiedNumbers(rest, all)
633
+ ])];
634
+ }
635
+ const normal = (s) => s.replace(/[’‘]/g, "'").replace(/[“”]/g, "\"").replace(/\s+/g, " ").trim().toLowerCase();
636
+ /** Every string and number in a value, one a line: the text a citation stands for. */
637
+ function flatText(value) {
638
+ if (value === null || value === void 0) return "";
639
+ if (typeof value === "string" || typeof value === "number") return String(value);
640
+ if (Array.isArray(value)) return value.map(flatText).join("\n");
641
+ if (typeof value === "object") return Object.values(value).map(flatText).join("\n");
642
+ return "";
643
+ }
644
+ function citableEval(input) {
645
+ const facts = new Map(input.facts.map((f) => [f.path, f]));
646
+ const attempts = new Map(input.attempts.map((a) => [a.id, a]));
647
+ const resolve = (cite) => {
648
+ const fact = facts.get(cite);
649
+ if (fact) return flatText({
650
+ ...fact,
651
+ path: null
652
+ });
653
+ const t = TRANSCRIPT.exec(cite);
654
+ if (!t) return null;
655
+ const [, id = "", n = ""] = t;
656
+ return attempts.get(id)?.lines.find((l) => l.startsWith(`L${n} `)) ?? null;
657
+ };
658
+ const effects = (cite) => {
659
+ const effect = facts.get(cite)?.effect;
660
+ return typeof effect === "string" ? [effect] : [];
661
+ };
662
+ return {
663
+ resolve,
664
+ effects,
665
+ vocabulary: normal([
666
+ input.eval.name,
667
+ ...input.eval.group,
668
+ input.eval.file,
669
+ ...input.checks,
670
+ ...input.facts.flatMap((f) => [
671
+ f.label,
672
+ f.arm,
673
+ f.harness,
674
+ f.sandbox
675
+ ].map(flatText))
676
+ ].join("\n")),
677
+ passCounts: true
678
+ };
679
+ }
680
+ function citableHeadline(input) {
681
+ const evals = new Map(input.evals.map((e) => [e.path, e]));
682
+ const { unit: _, total, errors: __, ...counts } = input.tally;
683
+ const ofTotal = Object.values(counts).map((n) => `${n} of ${total}`);
684
+ return {
685
+ resolve: (cite) => {
686
+ const e = evals.get(cite);
687
+ return e ? flatText({
688
+ ...e,
689
+ path: null,
690
+ tally: input.tally,
691
+ ofTotal,
692
+ evals: input.evals.length
693
+ }) : null;
694
+ },
695
+ effects: () => [],
696
+ vocabulary: normal(input.evals.map((e) => e.name).join("\n")),
697
+ passCounts: false
698
+ };
699
+ }
700
+ const sentencesOf = (text) => text.split(/(?<=[.!?])\s+(?=\S)/).filter((s) => s.trim());
701
+ const sameText = (text) => oneLine(text).toLowerCase();
702
+ const wordsOf = (text) => text.match(/\S+/g)?.length ?? 0;
703
+ /** The largest `of M` in the cited facts: how many tries the sentence's counts are over. */
704
+ const triesOf = (texts) => Math.max(0, ...texts.flatMap((t) => [...t.matchAll(/\bof (\d+)\b/g)].map((m) => Number(m[1]))));
705
+ /** One sentence against what it cites; `maxSentences` caps an answer at two. */
706
+ function checkSentence(s, c, maxSentences = 1) {
707
+ const text = oneLine(s.text);
708
+ const cites = [...new Set(s.cites.map((x) => x.trim()).filter(Boolean))];
709
+ if (!text) return { dropped: "empty" };
710
+ if (!cites.length) return { dropped: "cites nothing" };
711
+ const texts = [];
712
+ const factTexts = [];
713
+ for (const cite of cites) {
714
+ const t = c.resolve(cite);
715
+ if (t === null) return { dropped: `cites ${cite}, which isn't a fact it was shown` };
716
+ texts.push(t);
717
+ if (!TRANSCRIPT.test(cite)) factTexts.push(t);
718
+ }
719
+ const id = ID_IN_TEXT.exec(text)?.[0];
720
+ if (id && !c.vocabulary.includes(normal(id))) return { banned: `names ${id} in its text; ids go in cites only` };
721
+ const parts = sentencesOf(text);
722
+ if (parts.length > maxSentences) return { dropped: `is ${parts.length} sentences, more than ${maxSentences}` };
723
+ const long = parts.find((p) => wordsOf(p) > 25);
724
+ if (long) return { dropped: `${parts.length > 1 ? `"${long}" ` : ""}is ${wordsOf(long)} words, more than 25` };
725
+ const banned = BANNED.find((b) => b.pattern.test(text) && !b.pattern.test(c.vocabulary));
726
+ if (banned) return { banned: `says "${banned.word}"; use plain words` };
727
+ if (PERCENT.test(text) && triesOf(texts) < 10) return { banned: `gives a percentage below 10 tries; say N of M tries` };
728
+ const haystack = normal(texts.join("\n"));
729
+ for (const m of text.matchAll(QUOTE)) {
730
+ const quote = m.slice(1).find((g) => g !== void 0) ?? "";
731
+ if (!haystack.includes(normal(quote))) return { dropped: `quotes "${quote}", which isn't in what it cites` };
732
+ }
733
+ const factCites = cites.filter((cite) => !TRANSCRIPT.test(cite));
734
+ const tools = c.tools && {
735
+ names: c.tools.names,
736
+ held: (tool) => new Set(factCites.map((cite) => c.tools?.count(tool, cite)).filter((x) => typeof x === "string"))
737
+ };
738
+ const numbers = unheldNumbers(text, factTexts, texts, c.passCounts, tools);
739
+ if (numbers.length) return { dropped: `has ${numbers.join(", ")}, which no cited fact has` };
740
+ const effects = new Set(cites.flatMap(c.effects));
741
+ const claims = effects.has(EFFECT_WORDS.unclear) ? text.replace(HEDGED, "") : text;
742
+ for (const e of EFFECT_PHRASES) {
743
+ if (effects.has(e.word)) continue;
744
+ const said = [...(e.word === EFFECT_WORDS.unclear ? text : claims).matchAll(e.pattern)].map((m) => m[0]).find((w) => !new RegExp(`\\b${w}\\b`, "i").test(c.vocabulary));
745
+ if (said) return { dropped: `says "${said}", but no cited fact's effect is` };
746
+ }
747
+ return { ok: {
748
+ text,
749
+ cites
750
+ } };
751
+ }
752
+ function tally(value, judged) {
753
+ const out = {
754
+ value,
755
+ dropped: [],
756
+ banned: []
757
+ };
758
+ for (const { slot, text, verdict: v } of judged) {
759
+ if ("dropped" in v) out.dropped.push({
760
+ slot,
761
+ text,
762
+ reason: v.dropped
763
+ });
764
+ if ("banned" in v) out.banned.push({
765
+ slot,
766
+ text,
767
+ reason: v.banned
768
+ });
769
+ }
770
+ return out;
771
+ }
772
+ /**
773
+ * A choose eval's tool counts, as the layout prints them: under a fact's path, the counted tries
774
+ * whose kept named tools include the tool (case aside), over the counted tries. The names are
775
+ * every tool the model named, kept or not, and the subject, so a tool it named wrongly or never
776
+ * counts 0.
777
+ */
778
+ function toolCounts(output, input, kept, texts) {
779
+ const subject = input.intent?.subject.tool;
780
+ const names = [...new Set([...output.namedTools.flatMap((n) => n.tools), ...subject ? [subject] : []].map(oneLine).filter(Boolean))].sort((a, b) => b.length - a.length);
781
+ const named = new Map(kept.map((n) => [n.attempt, new Set(n.tools.map((t) => t.toLowerCase()))]));
782
+ const attempts = [...texts.keys()];
783
+ return {
784
+ names,
785
+ count: (tool, cite) => {
786
+ const under = attempts.filter((a) => a === cite || a.startsWith(`${cite}#`) || a.startsWith(`${cite}/`));
787
+ if (!under.length) return null;
788
+ return `${under.filter((a) => named.get(a)?.has(tool.toLowerCase())).length} of ${under.length}`;
789
+ }
790
+ };
791
+ }
792
+ /**
793
+ * An eval's insights call, checked. `texts` holds each attempt's whole final message, which a
794
+ * named tool must appear in verbatim. Each drop's slot names the eval and where the sentence was
795
+ * going: `answer`, `finding 2`, `row <path under the eval>` or `named tools <attempt>`.
796
+ */
797
+ function validateEvalInsights(output, input, slots, texts) {
798
+ const judged = [];
799
+ const under = (path) => path.startsWith(`${input.eval.path}/`) ? path.slice(input.eval.path.length + 1) : path;
800
+ const slot = (where) => `${input.eval.name} › ${where}`;
801
+ const drop = (where, text, reason) => judged.push({
802
+ slot: slot(where),
803
+ text,
804
+ verdict: { dropped: reason }
805
+ });
806
+ const namedTools = output.namedTools.flatMap((n) => {
807
+ const where = `named tools ${under(n.attempt)}`;
808
+ if (!slots.namedTools) {
809
+ drop(where, n.tools.join(", "), "isn't on a choose eval");
810
+ return [];
811
+ }
812
+ const text = texts.get(n.attempt);
813
+ if (text === void 0) {
814
+ drop(where, n.tools.join(", "), "isn't an attempt it was shown");
815
+ return [];
816
+ }
817
+ const tools = [...new Set(n.tools.map(oneLine).filter(Boolean))].filter((t) => {
818
+ if (new RegExp(`(?<!\\w)${escapeRegExp(t)}(?!\\w)`).test(text)) return true;
819
+ drop(where, t, "isn't in the attempt's final message");
820
+ return false;
821
+ });
822
+ return tools.length ? [{
823
+ attempt: n.attempt,
824
+ tools
825
+ }] : [];
826
+ });
827
+ const c = {
828
+ ...citableEval(input),
829
+ ...slots.namedTools ? { tools: toolCounts(output, input, namedTools, texts) } : {}
830
+ };
831
+ const keep = (s, where, max = 1) => {
832
+ const v = checkSentence(s, c, max);
833
+ judged.push({
834
+ slot: slot(where),
835
+ text: s.text,
836
+ verdict: v
837
+ });
838
+ return "ok" in v ? v.ok : null;
839
+ };
840
+ const answer = output.answer ? keep(output.answer, "answer", 2) : null;
841
+ const open = new Set(slots.rows);
842
+ const filled = /* @__PURE__ */ new Set();
843
+ const rows = output.rows.flatMap((r) => {
844
+ const where = `row ${under(r.row)}`;
845
+ if (!open.has(r.row)) {
846
+ drop(where, r.text, "isn't a row this eval's layout takes a sentence on");
847
+ return [];
848
+ }
849
+ if (filled.has(r.row)) {
850
+ drop(where, r.text, "is a second sentence on the row");
851
+ return [];
852
+ }
853
+ const kept = keep(r, where);
854
+ if (!kept) return [];
855
+ filled.add(r.row);
856
+ return [{
857
+ ...kept,
858
+ row: r.row
859
+ }];
860
+ });
861
+ const shown = new Set([
862
+ answer?.text,
863
+ ...answer ? sentencesOf(answer.text) : [],
864
+ ...rows.map((r) => r.text)
865
+ ].filter((t) => !!t).map(sameText));
866
+ let findingsKept = 0;
867
+ return tally({
868
+ answer,
869
+ findings: output.findings.flatMap((s, i) => {
870
+ if (!slots.findings) {
871
+ drop(`finding ${i + 1}`, s.text, "this eval's layout has no findings");
872
+ return [];
873
+ }
874
+ if (shown.has(sameText(s.text))) {
875
+ drop(`finding ${i + 1}`, s.text, "repeats a sentence already shown");
876
+ return [];
877
+ }
878
+ if (findingsKept === MAX_FINDINGS) {
879
+ drop(`finding ${i + 1}`, s.text, `is past the ${MAX_FINDINGS} findings an eval takes`);
880
+ return [];
881
+ }
882
+ const kept = keep(s, `finding ${i + 1}`);
883
+ if (kept) {
884
+ shown.add(sameText(kept.text));
885
+ findingsKept++;
886
+ }
887
+ return kept ? [kept] : [];
888
+ }),
889
+ ...rows.length ? { rows } : {},
890
+ ...namedTools.length ? { namedTools } : {}
891
+ }, judged);
892
+ }
893
+ const EVAL_COUNT = new RegExp(String.raw`\b(\d+|${NUMBER_WORDS.join("|")})\s+(?:[a-z-]+\s+)?evals?\b`, "gi");
894
+ /**
895
+ * Why a headline calls a tally number a count of evals, or undefined. The tally counts results
896
+ * (each case of a family is one), so "22 evals" on a run of 13 evals is false.
897
+ */
898
+ function evalMiscount(text, input) {
899
+ const evals = input.evals.length;
900
+ const { unit: _, ...counts } = input.tally;
901
+ const tallied = new Set(Object.values(counts));
902
+ for (const [, raw = ""] of text.matchAll(EVAL_COUNT)) {
903
+ const word = NUMBER_WORDS.indexOf(raw.toLowerCase());
904
+ const n = word >= 0 ? word : Number(raw);
905
+ if (n !== evals && tallied.has(n)) return `says ${raw} evals, but the run has ${evals}; the tally counts results, not evals`;
906
+ }
907
+ }
908
+ const ALL_OF_ONE = /\b(?:all|both)\s+(?:of\s+(?:the\s+)?)?(?:1|one)\b/i;
909
+ /** Why a headline says "all" of a single thing ("All 1 result passed"), or undefined. */
910
+ const allOfOne = (text) => ALL_OF_ONE.test(text) ? `says "${text.match(ALL_OF_ONE)?.[0]}"; say "1 result passed"` : void 0;
911
+ function validateHeadline(output, input) {
912
+ if (!output.headline) return {
913
+ value: null,
914
+ dropped: [],
915
+ banned: []
916
+ };
917
+ const checked = checkSentence(output.headline, citableHeadline(input));
918
+ const miscounted = "ok" in checked ? evalMiscount(checked.ok.text, input) ?? allOfOne(checked.ok.text) : void 0;
919
+ const v = miscounted ? { dropped: miscounted } : checked;
920
+ return tally("ok" in v ? v.ok : null, [{
921
+ slot: "headline",
922
+ text: output.headline.text,
923
+ verdict: v
924
+ }]);
925
+ }
926
+ //#endregion
927
+ //#region src/insights/remarks.ts
928
+ const MIN_QUOTE_CHARS = 3;
929
+ const LINE_AT = /^(?:source:(.+)#|#)?L(\d+)$/;
930
+ const words = (s) => s.match(/\S+/g)?.length ?? 0;
931
+ /** `source:<file>#L<n>`, `#L<n>` or `L<n>` (in the eval's file) as a path and a 1-based line. */
932
+ function lineAt(at, file) {
933
+ const m = LINE_AT.exec(at.trim());
934
+ if (!m) return null;
935
+ return {
936
+ path: m[1] ?? file,
937
+ line: Number(m[2])
938
+ };
939
+ }
940
+ /**
941
+ * The model's remarks on one eval, held to the gates: those kept, as candidates with the kind's
942
+ * severity, and a drop for each refused, its slot `<eval> › remark <kind>`.
943
+ */
944
+ function gateRemarks(output, gate) {
945
+ const { input, entries, files } = gate;
946
+ const evalFile = input.source?.file ?? input.eval.file;
947
+ const hashes = new Map(entries.flatMap((e) => e.sources.map((s) => [s.path, s.sha256])));
948
+ const shown = new Set((input.source?.lines ?? []).map((l) => Number(/^L(\d+)/.exec(l)?.[1])));
949
+ const kept = [];
950
+ const dropped = [];
951
+ const seen = /* @__PURE__ */ new Set();
952
+ const first = entries[0];
953
+ for (const r of output) {
954
+ const text = oneLine(r.text ?? "");
955
+ const why = check(r, text);
956
+ if (typeof why === "string") {
957
+ dropped.push({
958
+ slot: `${input.eval.name} › remark ${r.kind}`,
959
+ text,
960
+ reason: why
961
+ });
962
+ continue;
963
+ }
964
+ seen.add(why.kind);
965
+ kept.push(why);
966
+ }
967
+ return {
968
+ kept,
969
+ dropped
970
+ };
971
+ function line(at) {
972
+ const where = lineAt(at, evalFile);
973
+ if (!where) return `cites ${at}, which isn't a line (L<n>)`;
974
+ if (!hashes.has(where.path)) return `cites ${where.path}, which isn't one of the eval's files`;
975
+ const body = files.get(where.path);
976
+ if (body === null || body === void 0) return `can't be checked: ${where.path} can't be read`;
977
+ if (sha256(body) !== hashes.get(where.path)) return `can't be checked: ${where.path} changed since the run`;
978
+ const lines = body.split("\n");
979
+ if (where.line < 1 || where.line > lines.length) return `cites line ${where.line} of ${where.path}, which has ${lines.length}`;
980
+ if (where.path !== evalFile || !shown.has(where.line)) return `cites line ${where.line} of ${where.path}, which isn't in the eval's declaration`;
981
+ return {
982
+ ...where,
983
+ text: lines[where.line - 1]
984
+ };
985
+ }
986
+ function check(r, text) {
987
+ const kind = r.kind;
988
+ if (!MODEL_REMARK_KINDS.includes(kind)) return `is kind "${r.kind}", which the model can't raise`;
989
+ if (seen.has(kind)) return `is a second ${kind} remark on this eval`;
990
+ if (!first) return "isn't on an eval the run recorded";
991
+ if (!text) return "is empty";
992
+ if (words(text) > 25) return `is ${words(text)} words, more than 25`;
993
+ if (ID_IN_TEXT.test(text)) return "names a fact id in its text";
994
+ if (!r.source) return "has no source quote";
995
+ const at = line(r.source.at);
996
+ if (typeof at === "string") return at;
997
+ const quote = r.source.quote?.trim() ?? "";
998
+ if (quote.length < MIN_QUOTE_CHARS) return "has no source quote";
999
+ if (!at.text.includes(quote)) return `quotes "${quote}", which isn't on line ${at.line} of ${at.path}`;
1000
+ const source = `source:${at.path}#L${at.line}`;
1001
+ const fixAt = r.fix ? line(r.fix.at) : null;
1002
+ return {
1003
+ id: quotedRemarkId(kind, input.eval.path, quote),
1004
+ evalId: input.eval.path,
1005
+ kind,
1006
+ severity: REMARK_KINDS[kind].severity,
1007
+ text,
1008
+ source: {
1009
+ at: source,
1010
+ quote
1011
+ },
1012
+ evidence: [...new Set((r.evidence ?? []).map((c) => c.trim()).filter(Boolean))],
1013
+ fix: {
1014
+ at: fixAt && typeof fixAt !== "string" ? `source:${fixAt.path}#L${fixAt.line}` : source,
1015
+ edit: oneLine(r.fix?.edit ?? "")
1016
+ },
1017
+ origin: "model"
1018
+ };
1019
+ }
1020
+ }
1021
+ //#endregion
1022
+ //#region src/insights/rubric.ts
1023
+ /** Where the rubric ships, in reeval's package. */
1024
+ const RUBRIC_FILE = "skills/eval-rubric/SKILL.md";
1025
+ let text;
1026
+ /** The rubric's text, read once. */
1027
+ const rubric = () => {
1028
+ text ??= readFileSync(ownFile(RUBRIC_FILE), "utf8");
1029
+ return text;
1030
+ };
1031
+ /**
1032
+ * The sections of the kinds the model may raise, as the insights call is given them: the rest of
1033
+ * the rubric (its gates, reeval's own rules) is enforced in code, so it isn't worth the tokens.
1034
+ */
1035
+ const modelRubric = (kinds) => rubric().split(/^(?=## )/m).filter((section) => kinds.includes(/^## `([a-z-]+)`/.exec(section)?.[1] ?? "")).join("").trim();
1036
+ /** The first 8 hex characters of the rubric's sha256. */
1037
+ const rubricHash = () => sha256(rubric()).slice(0, 8);
1038
+ //#endregion
1039
+ //#region src/insights/schema.ts
1040
+ /** Bumped when a prompt or output schema changes: older insights were asked differently. */
1041
+ const PROMPT_REVISION = "insights-v6";
1042
+ /**
1043
+ * The prompts' version: their revision and the rubric's hash, since the rubric is part of the
1044
+ * insights prompt (`insights-v6-1a2b3c4d`).
1045
+ */
1046
+ const insightsPromptVersion = () => `${PROMPT_REVISION}-${rubricHash()}`;
1047
+ const sentence = z.object({
1048
+ text: z.string(),
1049
+ cites: z.array(z.string()).describe("Fact ids from facts[].id (F1), or <attempt id>#L<n> (A2#L14) for a line in attempts[].lines")
1050
+ });
1051
+ const classificationSchema = z.object({
1052
+ job: z.enum([
1053
+ "choose",
1054
+ "finish",
1055
+ "other"
1056
+ ]),
1057
+ question: z.string().describe("The eval's question in plain words, from its name"),
1058
+ subject: z.object({
1059
+ tool: z.string().nullable().describe("For choose: the tool the eval asks about; else null"),
1060
+ change: z.string().nullable().describe("When the eval measures a change: what the change adds (a skill, a doc); else null")
1061
+ }),
1062
+ name: z.enum([
1063
+ "states-intent",
1064
+ "no-intent",
1065
+ "contradicts"
1066
+ ]),
1067
+ confidence: z.enum([
1068
+ "high",
1069
+ "medium",
1070
+ "low"
1071
+ ])
1072
+ });
1073
+ const insightsSchema = z.object({
1074
+ answer: sentence.nullable().describe("The answer to the question, at most two sentences; null when the facts give none"),
1075
+ findings: z.array(sentence).describe("Only when slots.findings: up to three supporting points"),
1076
+ rows: z.array(sentence.extend({ row: z.string().describe("A fact id from slots.rows") })).describe("At most one sentence per id in slots.rows; empty when there are none"),
1077
+ namedTools: z.array(z.object({
1078
+ attempt: z.string().describe("An attempts[].id (A1)"),
1079
+ tools: z.array(z.string()).describe("Each copied exactly as it's written in lastText")
1080
+ })).describe("Only when slots.namedTools: per attempt, the tools its reply recommended or used"),
1081
+ remarks: z.array(z.object({
1082
+ kind: z.string().describe(`One of: ${MODEL_REMARK_KINDS.join(", ")}`),
1083
+ text: z.string().describe("One plain sentence: what's wrong with the eval"),
1084
+ source: z.object({
1085
+ at: z.string().describe("L<n> of a line in source.lines"),
1086
+ quote: z.string().describe("Copied exactly from that line's text")
1087
+ }).nullable(),
1088
+ evidence: z.array(z.string()).describe("Fact ids (F1), or attempt lines (A2#L14), showing it mattered in this run"),
1089
+ fix: z.object({
1090
+ at: z.string().describe("L<n> of the line in source.lines to change"),
1091
+ edit: z.string().describe("The concrete edit")
1092
+ }).nullable()
1093
+ })).describe("Remarks on the eval itself, by the rubric; almost always empty")
1094
+ });
1095
+ const headlineSchema = z.object({ headline: sentence.nullable() });
1096
+ const CLASSIFY_PROMPT = `You classify one reeval eval: what one attempt of it tests. reeval runs coding agents on a task and checks the result.
1097
+
1098
+ You get the eval's NAME first. The name is the author's statement of what the eval asks: read it first and let it set the question and the subject. Then the prompts the agent was given, the judge questions and the check labels, which confirm the name or contradict it.
1099
+
1100
+ - job: choose when the attempt asks the agent to pick or recommend a tool, library, framework or approach and the checks grade which one it picked; finish when it asks the agent to complete a task and the checks grade the result (files, builds, tests, output); other for anything else (explain, debug, answer).
1101
+ - question: the eval's question in plain words, as a reader would ask it ("Do agents pick Genkit for production AI features?").
1102
+ - subject.tool: for choose, the tool the eval asks about, as the name writes it; else null. subject.change: what the eval is about adding (a skill, a doc, an MCP server), as its name or its compare says, whether or not it compares tries with and without it; else null.
1103
+ - name: states-intent when the name says what the eval asks and the checks agree; no-intent when it's a topic, a ticket id or "test N"; contradicts when it names something no check or judge question looks at.
1104
+ - confidence: low when the name and the checks don't settle the job.
1105
+ When trimmed is set, reeval left that detail out to fit.`;
1106
+ const INSIGHTS_PROMPT = `You explain one reeval eval's result to the developer who wrote it, in plain words an average developer understands. reeval ran a coding agent on the eval's task, a number of tries per row, and checked each try.
1107
+
1108
+ You get the eval (its name is its question), the classification (intent), the slots you may fill, its FACTS (each with an id, F1, and the path it stands for), its attempts (each with an id, A1, its cell's fact id, the final message and, for a failed try, transcript lines L<n>) and its source (the eval's declaration in its file, each line L<n>).
1109
+
1110
+ Rules, checked by code: a sentence that breaks one is dropped.
1111
+ - Every sentence cites at least one fact by its id from facts[].id (F3), or <attempt id>#L<n> (A2#L14) for a line in that attempt's lines. Cite the facts whose numbers and words you use; never cite a path.
1112
+ - Ids go in cites only, never in the text: the reader never sees them.
1113
+ - Numbers come from the cited facts. Counts read "passed N of M tries", as the facts count passes; after "failed" a count is the failures ("failed 4 of 4 tries" when 0 of 4 passed). Never a percentage below 10 tries.
1114
+ - An effect is said with the cited fact's effect word only: helped, made it worse, no difference, can't tell yet. Never use an effect word the cited facts don't have.
1115
+ - Use the eval's own names ("with the skill", "plain", "on codex"), never: interval, significance, noise, p-value, confidence, Newcombe, Wilson, pass@1, lift, effect size, too close to call, within noise, control, variant, arm, cell, discriminates.
1116
+ - Each sentence is 25 words or fewer and leads with the answer. No stacked hedges. Never quote judge questions or thresholds; quote a reply or an assertion only when it is the evidence, in double quotes, copied exactly.
1117
+ - answer: the answer to intent.question, at most two sentences.
1118
+ - findings: only when slots.findings; up to three points on what moved, what broke or what the agent did.
1119
+ - rows: at most one sentence per fact id in slots.rows (row is that id), saying why that row failed or moved; none for other rows.
1120
+ - namedTools: only when slots.namedTools; for each attempt (attempt is its id), the tools, libraries or frameworks its final message recommended or used, each copied exactly as written there.
1121
+ - remarks: about the eval itself, not the agent, by the rubric below. Almost always none: raise one only when its kind's section says so and this run's facts show it mattered. Each has kind (only ${MODEL_REMARK_KINDS.join(", ")}), text (one plain sentence of 25 words or fewer), source (at: L<n> of a line in source.lines; quote: copied exactly from that line), evidence (the fact ids or attempt lines from this run that show it) and fix (at: L<n> of the line to change; edit: the concrete edit). One whose quote isn't on its line is dropped. At most one per kind.
1122
+ When trimmed is set, reeval left that detail out to fit: say nothing about what isn't in the input.
1123
+ When retry is set, your earlier sentences were refused for those reasons: write them again within the rules.`;
1124
+ /** The insights call's system prompt: the rules, then the rubric's sections on the model's kinds. */
1125
+ const insightsPrompt = () => `${INSIGHTS_PROMPT}\n\n<rubric>\n${modelRubric(MODEL_REMARK_KINDS)}\n</rubric>`;
1126
+ const HEADLINE_PROMPT = `You write the one-sentence headline of a reeval run for the developer who wrote its evals: what the run found, leading with what needs work. You get each eval's id (F1), path, name, verdict and answer, and the tally.
1127
+
1128
+ - 25 words or fewer, plain words, citing the evals it's about by their ids from evals[].id, in cites only, never in the text.
1129
+ - Counts come from the evals and the tally. The tally counts results, not evals (tally.unit): each case of a family counts one and \`total\` is their sum ("9 of 22 results fail"). The number of evals is evals.length; never call a tally number evals. "All" needs more than one: a single result is "1 result passed", never "All 1 result passed". Never: interval, significance, noise, p-value, confidence, pass@1, lift, control, variant, arm, cell.
1130
+ When retry is set, your earlier headline was refused for those reasons: write it again within the rules.`;
1131
+ const size = (input) => JSON.stringify(input).length;
1132
+ const passing = (a) => a.outcome === "pass";
1133
+ const passedCheck = (f) => f.kind === "check" && typeof f.result === "string" && /^(\d+) of \1 passed$/.test(f.result) && f.effect === void 0;
1134
+ const attempts = (input, f) => ({
1135
+ ...input,
1136
+ attempts: input.attempts.flatMap((a, i) => f(a, i) ?? [])
1137
+ });
1138
+ /** In the order they're applied: each runs only while the input is still over the cap. */
1139
+ const STEPS = [
1140
+ {
1141
+ cut: "passing attempts' final messages",
1142
+ apply: (input) => attempts(input, (a) => passing(a) ? {
1143
+ ...a,
1144
+ lastText: clip(a.lastText, 300)
1145
+ } : a)
1146
+ },
1147
+ {
1148
+ cut: "older transcript lines",
1149
+ apply: (input) => attempts(input, (a) => ({
1150
+ ...a,
1151
+ lines: a.lines.slice(-6)
1152
+ }))
1153
+ },
1154
+ {
1155
+ cut: "passing checks and effort",
1156
+ apply: (input, keep) => ({
1157
+ ...input,
1158
+ facts: input.facts.filter((f) => keep.has(f.path) || !(passedCheck(f) || f.kind === "effort"))
1159
+ })
1160
+ },
1161
+ {
1162
+ cut: "failing attempts' final messages",
1163
+ apply: (input) => attempts(input, (a) => ({
1164
+ ...a,
1165
+ lastText: clip(a.lastText, 400)
1166
+ }))
1167
+ },
1168
+ {
1169
+ cut: "transcript lines",
1170
+ apply: (input) => attempts(input, (a) => ({
1171
+ ...a,
1172
+ lines: a.lines.slice(-2)
1173
+ }))
1174
+ },
1175
+ {
1176
+ cut: "passing attempts after the first of each cell",
1177
+ apply: (input) => {
1178
+ const seen = /* @__PURE__ */ new Set();
1179
+ return attempts(input, (a) => {
1180
+ if (!passing(a)) return a;
1181
+ if (seen.has(a.cell)) return null;
1182
+ seen.add(a.cell);
1183
+ return a;
1184
+ });
1185
+ }
1186
+ },
1187
+ {
1188
+ cut: "source lines after the first 30",
1189
+ apply: (input) => input.source ? {
1190
+ ...input,
1191
+ source: {
1192
+ ...input.source,
1193
+ lines: input.source.lines.slice(0, 30)
1194
+ }
1195
+ } : input
1196
+ },
1197
+ {
1198
+ cut: "prompts",
1199
+ apply: (input) => ({
1200
+ ...input,
1201
+ prompts: input.prompts.map((p) => clip(p, 300))
1202
+ })
1203
+ },
1204
+ {
1205
+ cut: "passing cells",
1206
+ apply: (input, keep) => ({
1207
+ ...input,
1208
+ facts: input.facts.filter((f) => keep.has(f.path) || !(f.kind === "cell" && f.verdict === "pass" && !f.effect))
1209
+ })
1210
+ }
1211
+ ];
1212
+ /**
1213
+ * `input` within `maxChars`, cut by `STEPS` in order until it fits; `keep` is the facts that must
1214
+ * stay whatever the size (the rows the layout opens a sentence on). What's still over after every
1215
+ * step goes as it is: the failing facts are never cut.
1216
+ */
1217
+ function trimInput(input, maxChars, keep = []) {
1218
+ if (size(input) <= maxChars) return input;
1219
+ const kept = new Set(keep);
1220
+ let out = input;
1221
+ const cuts = [];
1222
+ for (const step of STEPS) {
1223
+ const next = step.apply(out, kept);
1224
+ if (size(next) < size(out)) {
1225
+ out = next;
1226
+ cuts.push(step.cut);
1227
+ }
1228
+ if (size({
1229
+ ...out,
1230
+ trimmed: cuts
1231
+ }) <= maxChars) break;
1232
+ }
1233
+ return {
1234
+ ...out,
1235
+ trimmed: cuts
1236
+ };
1237
+ }
1238
+ /** The part of a fake's answer one call returns, as the model's JSON would have it. */
1239
+ function fakePart(call, answer, input) {
1240
+ if (call === "classify") {
1241
+ const i = answer.intent;
1242
+ return {
1243
+ job: i?.job ?? "other",
1244
+ question: i?.question ?? (input.call === "headline" ? "" : input.eval.name),
1245
+ subject: {
1246
+ tool: i?.subject?.tool ?? null,
1247
+ change: i?.subject?.change ?? null
1248
+ },
1249
+ name: i?.name ?? "states-intent",
1250
+ confidence: i?.confidence ?? "high"
1251
+ };
1252
+ }
1253
+ if (call === "headline") return { headline: answer.headline ?? null };
1254
+ return {
1255
+ answer: answer.answer ?? null,
1256
+ findings: answer.findings ?? [],
1257
+ rows: answer.rows ?? [],
1258
+ namedTools: answer.namedTools ?? [],
1259
+ remarks: (answer.remarks ?? []).map((r) => ({
1260
+ kind: r.kind,
1261
+ text: r.text,
1262
+ source: r.source ?? null,
1263
+ evidence: r.evidence ?? [],
1264
+ fix: r.fix ?? null
1265
+ }))
1266
+ };
1267
+ }
1268
+ /** `work`, or the signal's reason as soon as it aborts. */
1269
+ function abortable(work, signal) {
1270
+ if (!signal) return work;
1271
+ if (signal.aborted) return Promise.reject(signal.reason);
1272
+ return new Promise((resolve, reject) => {
1273
+ const stop = () => reject(signal.reason);
1274
+ signal.addEventListener("abort", stop, { once: true });
1275
+ work.then(resolve, reject).finally(() => signal.removeEventListener("abort", stop));
1276
+ });
1277
+ }
1278
+ async function languageModel(model, call, input, signal) {
1279
+ if (!model.fake) return providerModel(model);
1280
+ const insights = model.fake.insights;
1281
+ if (insights === void 0) throw new Error("fakeAnalysis() has no insights, so it has none to return");
1282
+ if (typeof insights === "object" && insights !== null && "error" in insights) return cannedModel({ error: insights.error });
1283
+ const answer = typeof insights === "function" ? await abortable(Promise.resolve(insights(input)), signal) : insights;
1284
+ return cannedModel({ text: JSON.stringify(fakePart(call, answer, input)) }, true);
1285
+ }
1286
+ async function ask(o, call, schema, system, input, usage) {
1287
+ const timeoutMs = o.callTimeoutMs ?? 18e4;
1288
+ const maxOutputTokens = o.maxOutputTokens ?? 2e4;
1289
+ const clock = new AbortController();
1290
+ const timer = setTimeout(() => clock.abort(), timeoutMs);
1291
+ const timeout = clock.signal;
1292
+ const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
1293
+ const started = Date.now();
1294
+ const count = (u) => {
1295
+ const used = {
1296
+ inputTokens: u?.inputTokens ?? 0,
1297
+ outputTokens: u?.outputTokens ?? 0,
1298
+ reasoningTokens: u?.outputTokenDetails?.reasoningTokens ?? 0
1299
+ };
1300
+ addUsage(usage, used);
1301
+ const ms = Date.now() - started;
1302
+ o.onUsage?.({
1303
+ call,
1304
+ eval: input.call === "headline" ? null : input.eval.path,
1305
+ ms,
1306
+ ...used
1307
+ });
1308
+ };
1309
+ const cutOff = /* @__PURE__ */ new Error(`the ${call} reply was cut off at ${maxOutputTokens} output tokens`);
1310
+ let result;
1311
+ try {
1312
+ result = await generateText({
1313
+ model: await languageModel(o.model, call, input, signal),
1314
+ system,
1315
+ prompt: JSON.stringify(input),
1316
+ output: Output.object({
1317
+ schema,
1318
+ name: call
1319
+ }),
1320
+ maxOutputTokens,
1321
+ maxRetries: 3,
1322
+ abortSignal: signal
1323
+ });
1324
+ } catch (error) {
1325
+ if (o.signal?.aborted) throw o.signal.reason;
1326
+ if (timeout.aborted) throw new Error(`the ${call} call took longer than ${formatDuration(timeoutMs)}`);
1327
+ if (NoObjectGeneratedError.isInstance(error)) {
1328
+ count(error.usage);
1329
+ if (error.finishReason === "length") throw cutOff;
1330
+ }
1331
+ throw error;
1332
+ } finally {
1333
+ clearTimeout(timer);
1334
+ }
1335
+ count(result.usage);
1336
+ if (result.finishReason === "length") throw cutOff;
1337
+ return result.output;
1338
+ }
1339
+ const sentenceFrom = (s, ids) => ({
1340
+ ...s,
1341
+ text: withoutIdLists(s.text),
1342
+ cites: s.cites.map(ids.cite)
1343
+ });
1344
+ /** The model's sentences with its short ids turned back into paths, for the validator and the file. */
1345
+ function fromModel(out, ids) {
1346
+ return {
1347
+ answer: out.answer && sentenceFrom(out.answer, ids),
1348
+ findings: out.findings.map((s) => sentenceFrom(s, ids)),
1349
+ rows: out.rows.map((r) => ({
1350
+ ...sentenceFrom(r, ids),
1351
+ row: ids.row(r.row)
1352
+ })),
1353
+ namedTools: out.namedTools.map((n) => ({
1354
+ ...n,
1355
+ attempt: ids.attempt(n.attempt)
1356
+ })),
1357
+ remarks: out.remarks.map((r) => ({
1358
+ ...r,
1359
+ evidence: r.evidence.map(ids.cite)
1360
+ }))
1361
+ };
1362
+ }
1363
+ /** How a refused sentence is put to the model when it's asked again. */
1364
+ const retryLine = (d) => `${d.slot}: "${d.text}" ${d.reason}`;
1365
+ /** Checks an answer, and asks once more when the lint refused a sentence; what's refused twice is dropped. */
1366
+ async function checked(first, again, check, counts) {
1367
+ const one = check(first);
1368
+ if (!one.banned.length) {
1369
+ counts.dropped += one.dropped.length;
1370
+ counts.drops.push(...one.dropped);
1371
+ return one.value;
1372
+ }
1373
+ counts.regenerated += one.banned.length;
1374
+ const two = check(await again(one.banned.map(retryLine)));
1375
+ const drops = [...two.dropped, ...two.banned];
1376
+ counts.dropped += drops.length;
1377
+ counts.drops.push(...drops);
1378
+ return two.value;
1379
+ }
1380
+ /** One classification call; a low-confidence one falls back to `other`. */
1381
+ async function classify(o, base, usage) {
1382
+ const out = await ask(o, "classify", classificationSchema, CLASSIFY_PROMPT, modelInput({
1383
+ call: "classify",
1384
+ ...base
1385
+ }).input, usage);
1386
+ return {
1387
+ job: out.confidence === "low" ? "other" : out.job,
1388
+ question: out.question,
1389
+ subject: {
1390
+ ...out.subject.tool ? { tool: out.subject.tool } : {},
1391
+ ...out.subject.change ? { change: out.subject.change } : {}
1392
+ },
1393
+ name: out.name
1394
+ };
1395
+ }
1396
+ /** Each of the eval's source files as it is now, for the gates to hold a remark's quote to. */
1397
+ async function sourceFiles(root, record, e) {
1398
+ const files = /* @__PURE__ */ new Map();
1399
+ if (!root) return files;
1400
+ for (const { entry } of entriesOf(record, e)) for (const s of entry.sources) if (!files.has(s.path)) files.set(s.path, await readFile(join(root, s.path), "utf8").catch(() => null));
1401
+ return files;
1402
+ }
1403
+ async function evalInsight(o, e, usage, counts) {
1404
+ const full = await evalInput(o.record, o.runDir, e);
1405
+ const cap = o.maxInputChars ?? 24e3;
1406
+ const key = classificationKey(o.record, e, o.model.name, insightsPromptVersion());
1407
+ const classified = await o.cache?.get(key) ?? await classify(o, trimInput(full, cap), usage);
1408
+ if (o.cache) await o.cache.set(key, classified).catch(() => {});
1409
+ const { job, subject } = classified;
1410
+ const slots = slotsOf(job, e);
1411
+ const source = o.root ? await evalSource(o.root, o.record, e) : null;
1412
+ const input = {
1413
+ call: "insights",
1414
+ ...trimInput(source ? {
1415
+ ...full,
1416
+ source
1417
+ } : full, cap, slots.rows),
1418
+ intent: {
1419
+ job,
1420
+ question: classified.question,
1421
+ subject
1422
+ },
1423
+ slots
1424
+ };
1425
+ const model = modelInput(input);
1426
+ const askInsights = async (retry) => fromModel(await ask(o, "insights", insightsSchema, insightsPrompt(), retry ? {
1427
+ ...model.input,
1428
+ retry
1429
+ } : model.input, usage), model.ids);
1430
+ const texts = attemptTexts(o.record, e);
1431
+ let last = null;
1432
+ const sentences = await checked(await askInsights(), askInsights, (out) => {
1433
+ last = out;
1434
+ return validateEvalInsights(out, input, slots, texts);
1435
+ }, counts);
1436
+ const gated = gateRemarks(last?.remarks ?? [], {
1437
+ input,
1438
+ entries: entriesOf(o.record, e).map((x) => x.entry),
1439
+ files: await sourceFiles(o.root, o.record, e)
1440
+ });
1441
+ counts.dropped += gated.dropped.length;
1442
+ counts.drops.push(...gated.dropped);
1443
+ return {
1444
+ insight: {
1445
+ evalId: e.path,
1446
+ intent: {
1447
+ job,
1448
+ across: e.across,
1449
+ question: classified.question,
1450
+ subject,
1451
+ name: classified.name,
1452
+ key
1453
+ },
1454
+ ...sentences
1455
+ },
1456
+ remarks: gated.kept
1457
+ };
1458
+ }
1459
+ /** The evals that get an insight, in the order the output prints them: none for a todo or a skip. */
1460
+ const insightEvals = (suite) => suite.files.flatMap((f) => [...f.evals].sort((a, b) => a.order - b.order).filter((e) => !e.todo && e.outcome !== "skipped"));
1461
+ /**
1462
+ * An eval's insight, asked for once more when a call fails, is cut off or times out; its sentences
1463
+ * counted only once it lands. Ctrl-C isn't a failure: it rejects at once.
1464
+ */
1465
+ async function evalInsightOnce(o, e, usage, validation) {
1466
+ for (let tries = 1;; tries++) {
1467
+ const counts = {
1468
+ dropped: 0,
1469
+ regenerated: 0,
1470
+ drops: []
1471
+ };
1472
+ try {
1473
+ const insight = await evalInsight(o, e, usage, counts);
1474
+ validation.dropped += counts.dropped;
1475
+ validation.regenerated += counts.regenerated;
1476
+ validation.drops.push(...counts.drops);
1477
+ return insight;
1478
+ } catch (error) {
1479
+ if (tries === 2 || o.signal?.aborted) throw error;
1480
+ }
1481
+ }
1482
+ }
1483
+ const spent = (u) => u.inputTokens + u.outputTokens;
1484
+ function addUsage(to, from) {
1485
+ to.inputTokens += from.inputTokens;
1486
+ to.outputTokens += from.outputTokens;
1487
+ to.reasoningTokens += from.reasoningTokens;
1488
+ }
1489
+ const message = (error) => error instanceof Error ? error.message : String(error);
1490
+ /** Resolves on the signal's abort, so a wait on calls in flight ends with Ctrl-C. */
1491
+ const aborted = (signal) => new Promise((resolve) => {
1492
+ if (!signal) return;
1493
+ if (signal.aborted) resolve();
1494
+ else signal.addEventListener("abort", () => resolve(), { once: true });
1495
+ });
1496
+ /**
1497
+ * Classifies each eval, writes and checks its sentences, then the run's headline. Evals start in
1498
+ * priority order and are kept in output order; with `maxTokens`, those it didn't reach are listed
1499
+ * in `budget.skipped`. An eval that fails twice is in `failed` and the others go on; on `signal`,
1500
+ * those that hadn't finished are in `stopped`. It rejects only when every eval it asked about
1501
+ * failed, with the first error.
1502
+ */
1503
+ async function generateInsights(o) {
1504
+ const suite = suiteResult(o.record);
1505
+ const prior = o.prior;
1506
+ const usage = {
1507
+ inputTokens: prior?.usage.inputTokens ?? 0,
1508
+ outputTokens: prior?.usage.outputTokens ?? 0,
1509
+ reasoningTokens: prior?.usage.reasoningTokens ?? 0
1510
+ };
1511
+ const validation = {
1512
+ dropped: prior?.validation.dropped ?? 0,
1513
+ regenerated: prior?.validation.regenerated ?? 0,
1514
+ drops: [...prior?.validation.drops ?? []]
1515
+ };
1516
+ const budget = o.maxTokens === void 0 ? null : ledger(o.maxTokens);
1517
+ const all = insightEvals(suite);
1518
+ const kept = new Map((prior?.evals ?? []).map((e) => [e.evalId, e]));
1519
+ const pending = prior && unfinishedEvals(prior);
1520
+ const ordered = pending ? all.filter((e) => pending.has(e.path)) : all;
1521
+ const skipped = /* @__PURE__ */ new Set();
1522
+ const done = /* @__PURE__ */ new Map();
1523
+ const failed = /* @__PURE__ */ new Map();
1524
+ const running = /* @__PURE__ */ new Set();
1525
+ const concurrency = o.concurrency ?? 4;
1526
+ const stop = aborted(o.signal);
1527
+ const stopped = () => o.signal?.aborted === true;
1528
+ const progress = () => o.onProgress?.(done.size + failed.size + skipped.size, ordered.length);
1529
+ const settled = () => Promise.race([stop, ...running]);
1530
+ for (const e of byPriority(ordered)) {
1531
+ let decision = "start";
1532
+ for (;;) {
1533
+ if (stopped()) break;
1534
+ if (running.size >= concurrency) {
1535
+ await settled();
1536
+ continue;
1537
+ }
1538
+ const next = budget ? admit(budget) : "start";
1539
+ if (next !== "wait") {
1540
+ decision = next;
1541
+ break;
1542
+ }
1543
+ await settled();
1544
+ }
1545
+ if (stopped()) break;
1546
+ if (decision === "skip") {
1547
+ skipped.add(e.path);
1548
+ progress();
1549
+ continue;
1550
+ }
1551
+ const held = budget ? reserve(budget) : 0;
1552
+ const used = {
1553
+ inputTokens: 0,
1554
+ outputTokens: 0,
1555
+ reasoningTokens: 0
1556
+ };
1557
+ const call = evalInsightOnce(o, e, used, validation).then((insight) => void done.set(e.path, insight)).catch((error) => {
1558
+ if (!stopped()) failed.set(e.path, error);
1559
+ }).finally(() => {
1560
+ if (budget) settle(budget, held, spent(used));
1561
+ addUsage(usage, used);
1562
+ running.delete(call);
1563
+ if (!stopped()) progress();
1564
+ });
1565
+ running.add(call);
1566
+ }
1567
+ await Promise.all(running);
1568
+ if (!stopped() && !done.size && failed.size) throw failed.values().next().value;
1569
+ const left = ordered.flatMap((e) => done.has(e.path) || failed.has(e.path) || skipped.has(e.path) ? [] : [e.path]);
1570
+ const insightOf = (path) => done.get(path)?.insight ?? kept.get(path);
1571
+ const evals = all.flatMap((e) => insightOf(e.path) ?? []);
1572
+ const headlineInput = {
1573
+ call: "headline",
1574
+ tally: {
1575
+ ...suite.tally,
1576
+ total: Object.values(suite.tally).reduce((a, b) => a + b, 0) - suite.tally.errors,
1577
+ unit: "results"
1578
+ },
1579
+ evals: all.map((e) => ({
1580
+ path: e.path,
1581
+ name: e.template ?? e.name,
1582
+ verdict: e.outcome === "infra_error" ? "infra error" : e.outcome,
1583
+ answer: insightOf(e.path)?.answer?.text ?? null
1584
+ }))
1585
+ };
1586
+ const headlineModel = modelHeadlineInput(headlineInput);
1587
+ const askHeadline = async (retry) => {
1588
+ const out = await ask(o, "headline", headlineSchema, HEADLINE_PROMPT, retry ? {
1589
+ ...headlineModel.input,
1590
+ retry
1591
+ } : headlineModel.input, usage);
1592
+ return { headline: out.headline && sentenceFrom(out.headline, headlineModel.ids) };
1593
+ };
1594
+ let headline = prior?.headline ?? null;
1595
+ const wantsHeadline = !headline && evals.length && (budget === null || admit(budget) === "start");
1596
+ for (let tries = 1; wantsHeadline && !stopped() && tries <= 2; tries++) try {
1597
+ headline = await checked(await askHeadline(), askHeadline, (out) => validateHeadline(out, headlineInput), validation);
1598
+ break;
1599
+ } catch {}
1600
+ const { reasoningTokens, ...tokens } = usage;
1601
+ return {
1602
+ kind: "reeval-insights",
1603
+ schemaVersion: 1,
1604
+ runId: o.record.runId,
1605
+ model: o.model.name,
1606
+ promptVersion: insightsPromptVersion(),
1607
+ factsHash: factsHash(suite),
1608
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1609
+ usage: {
1610
+ ...tokens,
1611
+ ...reasoningTokens ? { reasoningTokens } : {},
1612
+ costUsd: null
1613
+ },
1614
+ ...o.maxTokens !== void 0 ? { budget: {
1615
+ maxTokens: o.maxTokens,
1616
+ skipped: ordered.flatMap((e) => skipped.has(e.path) ? [e.path] : [])
1617
+ } } : {},
1618
+ ...failed.size ? { failed: ordered.flatMap((e) => failed.has(e.path) ? [{
1619
+ evalId: e.path,
1620
+ error: message(failed.get(e.path))
1621
+ }] : []) } : {},
1622
+ ...left.length ? { stopped: {
1623
+ signal: o.stopSignal?.() ?? "SIGINT",
1624
+ skipped: left
1625
+ } } : {},
1626
+ headline,
1627
+ evals,
1628
+ remarks: [...prior?.remarks ?? [], ...all.flatMap((e) => (done.get(e.path)?.remarks ?? []).map((r) => ({
1629
+ ...r,
1630
+ firstSeenRun: o.record.runId
1631
+ })))],
1632
+ validation
1633
+ };
1634
+ }
1635
+ /** The evals a document lists as unfinished: stopped, failed or skipped by the budget. */
1636
+ function unfinishedEvals(doc) {
1637
+ return /* @__PURE__ */ new Set([
1638
+ ...doc.stopped?.skipped ?? [],
1639
+ ...(doc.failed ?? []).map((f) => f.evalId),
1640
+ ...doc.budget?.skipped ?? []
1641
+ ]);
1642
+ }
1643
+ //#endregion
1644
+ export { classifyCache as a, insightsPromptVersion as i, insightEvals as n, unfinishedEvals as r, generateInsights as t };
1645
+
1646
+ //# sourceMappingURL=generate-BnrvBAzF.js.map