reeval 0.0.1

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 (213) hide show
  1. package/LICENSE +202 -0
  2. package/THIRD_PARTY_NOTICES.md +2160 -0
  3. package/assets/package.json +9 -0
  4. package/assets/pnpm-lock.yaml +1336 -0
  5. package/dist/agent-DVHkx8aT.js +532 -0
  6. package/dist/agent-DVHkx8aT.js.map +1 -0
  7. package/dist/analysis-CxSCt1YY.js +16 -0
  8. package/dist/analysis-CxSCt1YY.js.map +1 -0
  9. package/dist/assemble-B-nmz8oH.js +695 -0
  10. package/dist/assemble-B-nmz8oH.js.map +1 -0
  11. package/dist/assemble-CmRVDZ2Q.js +2 -0
  12. package/dist/attempt-BdRQ9jKX.js +176 -0
  13. package/dist/attempt-BdRQ9jKX.js.map +1 -0
  14. package/dist/bridge/index.mjs +1911 -0
  15. package/dist/bridge/package.json +13 -0
  16. package/dist/bridge/pnpm-lock.yaml +1041 -0
  17. package/dist/bridge/pnpm-workspace.yaml +2 -0
  18. package/dist/budget-DBUrH4jX.js +8 -0
  19. package/dist/budget-DBUrH4jX.js.map +1 -0
  20. package/dist/cli--CQ3Iy9q.js +1865 -0
  21. package/dist/cli--CQ3Iy9q.js.map +1 -0
  22. package/dist/cli-spec-DKAu09Gi.js +478 -0
  23. package/dist/cli-spec-DKAu09Gi.js.map +1 -0
  24. package/dist/cli.d.ts +1 -0
  25. package/dist/cli.js +8 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/colors-QV9dFVhN.js +34 -0
  28. package/dist/colors-QV9dFVhN.js.map +1 -0
  29. package/dist/command-BSJzHMtP.js +280 -0
  30. package/dist/command-BSJzHMtP.js.map +1 -0
  31. package/dist/command-CEm8PZ1N.js +1043 -0
  32. package/dist/command-CEm8PZ1N.js.map +1 -0
  33. package/dist/command-D-jnQWVG.js +673 -0
  34. package/dist/command-D-jnQWVG.js.map +1 -0
  35. package/dist/command-DQAvbG7d.js +432 -0
  36. package/dist/command-DQAvbG7d.js.map +1 -0
  37. package/dist/commands-A4udfyKZ.js +2 -0
  38. package/dist/commands-CF8mOfiu.js +207 -0
  39. package/dist/commands-CF8mOfiu.js.map +1 -0
  40. package/dist/config-Cuj0kVYj.js +415 -0
  41. package/dist/config-Cuj0kVYj.js.map +1 -0
  42. package/dist/context-loi9OCZv.js +106 -0
  43. package/dist/context-loi9OCZv.js.map +1 -0
  44. package/dist/core-tLN-z1Be.js +96 -0
  45. package/dist/core-tLN-z1Be.js.map +1 -0
  46. package/dist/db-Cjj2ZsfQ.js +2 -0
  47. package/dist/db-DxDzMhtB.js +827 -0
  48. package/dist/db-DxDzMhtB.js.map +1 -0
  49. package/dist/debug-D24NOQH1.js +89 -0
  50. package/dist/debug-D24NOQH1.js.map +1 -0
  51. package/dist/debug-SEmW4w8f.js +2 -0
  52. package/dist/dist-BZXYHN2X.js +1673 -0
  53. package/dist/dist-BZXYHN2X.js.map +1 -0
  54. package/dist/docker-DQDZWXF9.js +20 -0
  55. package/dist/docker-DQDZWXF9.js.map +1 -0
  56. package/dist/docker-DnWAVdEm.js +2 -0
  57. package/dist/docker-oSbCExHL.js +153 -0
  58. package/dist/docker-oSbCExHL.js.map +1 -0
  59. package/dist/doctor-CxlH1pDM.js +525 -0
  60. package/dist/doctor-CxlH1pDM.js.map +1 -0
  61. package/dist/egress-73SQfKdx.js +619 -0
  62. package/dist/egress-73SQfKdx.js.map +1 -0
  63. package/dist/env-BQqj93gu.js +21 -0
  64. package/dist/env-BQqj93gu.js.map +1 -0
  65. package/dist/environment-Mz7wvrtD.js +552 -0
  66. package/dist/environment-Mz7wvrtD.js.map +1 -0
  67. package/dist/errors-C9w_ud7g.js +160 -0
  68. package/dist/errors-C9w_ud7g.js.map +1 -0
  69. package/dist/errors-CSxfJ4qu.js +11 -0
  70. package/dist/errors-CSxfJ4qu.js.map +1 -0
  71. package/dist/files--6CEUXm7.js +2 -0
  72. package/dist/files-BLlrhU4-.js +62 -0
  73. package/dist/files-BLlrhU4-.js.map +1 -0
  74. package/dist/files-Bd25OAQH.js +2 -0
  75. package/dist/files-BvHCFWgD.js +2 -0
  76. package/dist/files-D2-qn_Dl.js +275 -0
  77. package/dist/files-D2-qn_Dl.js.map +1 -0
  78. package/dist/files-bF2fPuT9.js +99 -0
  79. package/dist/files-bF2fPuT9.js.map +1 -0
  80. package/dist/git-D6hMeieS.js +44 -0
  81. package/dist/git-D6hMeieS.js.map +1 -0
  82. package/dist/harness-ls5aX1UO.js +131 -0
  83. package/dist/harness-ls5aX1UO.js.map +1 -0
  84. package/dist/harness.d.ts +74 -0
  85. package/dist/harness.js +2 -0
  86. package/dist/header-DzFDoMCI.js +78 -0
  87. package/dist/header-DzFDoMCI.js.map +1 -0
  88. package/dist/ids-Bp6ut7ZD.js +49 -0
  89. package/dist/ids-Bp6ut7ZD.js.map +1 -0
  90. package/dist/images-Cc1-coFc.js +286 -0
  91. package/dist/images-Cc1-coFc.js.map +1 -0
  92. package/dist/index.d.ts +97 -0
  93. package/dist/index.js +1959 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/init-CFljS8BJ.js +344 -0
  96. package/dist/init-CFljS8BJ.js.map +1 -0
  97. package/dist/init-z2ZPn9Tj.js +2 -0
  98. package/dist/invocations-rXktgSAn.js +157 -0
  99. package/dist/invocations-rXktgSAn.js.map +1 -0
  100. package/dist/jiti-BOLXpKyG.js +46 -0
  101. package/dist/jiti-BOLXpKyG.js.map +1 -0
  102. package/dist/judge-D8Uf4dA9.js +146 -0
  103. package/dist/judge-D8Uf4dA9.js.map +1 -0
  104. package/dist/judge.d.ts +21 -0
  105. package/dist/judge.js +19 -0
  106. package/dist/judge.js.map +1 -0
  107. package/dist/live-Bs3LReMV.js +98 -0
  108. package/dist/live-Bs3LReMV.js.map +1 -0
  109. package/dist/matchers-DyLvm7EG.js +374 -0
  110. package/dist/matchers-DyLvm7EG.js.map +1 -0
  111. package/dist/model-BdO79gWz.js +44 -0
  112. package/dist/model-BdO79gWz.js.map +1 -0
  113. package/dist/model-CDL98xue.js +725 -0
  114. package/dist/model-CDL98xue.js.map +1 -0
  115. package/dist/model-v-0_Ik7S.js +2 -0
  116. package/dist/node-kG64XxdE.js +506 -0
  117. package/dist/node-kG64XxdE.js.map +1 -0
  118. package/dist/opencode-providers-DZuD_oYt.js +3149 -0
  119. package/dist/opencode-providers-DZuD_oYt.js.map +1 -0
  120. package/dist/options-CAQhu--C.js +452 -0
  121. package/dist/options-CAQhu--C.js.map +1 -0
  122. package/dist/patch-BkKje4cp.js +69 -0
  123. package/dist/patch-BkKje4cp.js.map +1 -0
  124. package/dist/paths-CBvm6pap.js +72 -0
  125. package/dist/paths-CBvm6pap.js.map +1 -0
  126. package/dist/plan-gwk6dJ8c.js +237 -0
  127. package/dist/plan-gwk6dJ8c.js.map +1 -0
  128. package/dist/preflight-BtubXKQ8.js +2 -0
  129. package/dist/preflight-BypZMNBG.js +185 -0
  130. package/dist/preflight-BypZMNBG.js.map +1 -0
  131. package/dist/prepare-Cwv0bHs_.js +3 -0
  132. package/dist/prepare-D6Uzcubb.js +213 -0
  133. package/dist/prepare-D6Uzcubb.js.map +1 -0
  134. package/dist/provenance-Dfb8YY5Y.js +129 -0
  135. package/dist/provenance-Dfb8YY5Y.js.map +1 -0
  136. package/dist/provider-BDbNOQhQ.js +2 -0
  137. package/dist/provider-CmAYTQQs.js +14 -0
  138. package/dist/provider-CmAYTQQs.js.map +1 -0
  139. package/dist/providers-2p3ylMHI.js +107 -0
  140. package/dist/providers-2p3ylMHI.js.map +1 -0
  141. package/dist/providers-BUOxitcT.js +2 -0
  142. package/dist/query-CFATHZmV.js +91 -0
  143. package/dist/query-CFATHZmV.js.map +1 -0
  144. package/dist/query-DR4m6CXf.js +2 -0
  145. package/dist/record-DQ8wzyco.js +99 -0
  146. package/dist/record-DQ8wzyco.js.map +1 -0
  147. package/dist/redact-C4fL_bZP.js +167 -0
  148. package/dist/redact-C4fL_bZP.js.map +1 -0
  149. package/dist/results-CeXb8Wde.js +37 -0
  150. package/dist/results-CeXb8Wde.js.map +1 -0
  151. package/dist/root-CFDjpWuc.js +110 -0
  152. package/dist/root-CFDjpWuc.js.map +1 -0
  153. package/dist/sandbox-By2gkdkC.js +133 -0
  154. package/dist/sandbox-By2gkdkC.js.map +1 -0
  155. package/dist/sandbox.d.ts +41 -0
  156. package/dist/sandbox.js +2 -0
  157. package/dist/server-Bi61uiY0.js +350 -0
  158. package/dist/server-Bi61uiY0.js.map +1 -0
  159. package/dist/server-CsSBL8NC.js +2 -0
  160. package/dist/source-BXzrmof5.js +2 -0
  161. package/dist/source-Bsfa3LZH.js +40 -0
  162. package/dist/source-Bsfa3LZH.js.map +1 -0
  163. package/dist/src-55j1zNHH.js +3 -0
  164. package/dist/src-AEwxd1UP.js +602 -0
  165. package/dist/src-AEwxd1UP.js.map +1 -0
  166. package/dist/src-BDwSuyIo.js +3 -0
  167. package/dist/src-BQyMiWri.js +490 -0
  168. package/dist/src-BQyMiWri.js.map +1 -0
  169. package/dist/src-C-LHyTSV.js +3 -0
  170. package/dist/src-Cmp-7YrD.js +1136 -0
  171. package/dist/src-Cmp-7YrD.js.map +1 -0
  172. package/dist/src-D0RdqpNb.js +252 -0
  173. package/dist/src-D0RdqpNb.js.map +1 -0
  174. package/dist/src-D8KYzxBX.js +793 -0
  175. package/dist/src-D8KYzxBX.js.map +1 -0
  176. package/dist/src-DEgff9Z7.js +4 -0
  177. package/dist/src-DXx_29pq.js +567 -0
  178. package/dist/src-DXx_29pq.js.map +1 -0
  179. package/dist/src-DwwiVLqv.js +2937 -0
  180. package/dist/src-DwwiVLqv.js.map +1 -0
  181. package/dist/src-N7E_mfcZ.js +2 -0
  182. package/dist/src-W9g-y5X5.js +3 -0
  183. package/dist/src-daS0wKH2.js +3 -0
  184. package/dist/state-Cyw3QTsN.js +2 -0
  185. package/dist/state-DZ7eqb1F.js +167 -0
  186. package/dist/state-DZ7eqb1F.js.map +1 -0
  187. package/dist/stats-BNAHlNMD.js +114 -0
  188. package/dist/stats-BNAHlNMD.js.map +1 -0
  189. package/dist/studio-app/assets/index-D6XyfFSH.js +58 -0
  190. package/dist/studio-app/assets/index-xwzWdcE_.css +2 -0
  191. package/dist/studio-app/favicon.svg +1 -0
  192. package/dist/studio-app/index.html +15 -0
  193. package/dist/summary-D7Ttky0z.js +166 -0
  194. package/dist/summary-D7Ttky0z.js.map +1 -0
  195. package/dist/text-gEcKjAyp.js +29 -0
  196. package/dist/text-gEcKjAyp.js.map +1 -0
  197. package/dist/types-C5vI3g1C.js +9 -0
  198. package/dist/types-C5vI3g1C.js.map +1 -0
  199. package/dist/types-DLec-6Cx.d.ts +1535 -0
  200. package/dist/usage-DsYUqIUM.js +24 -0
  201. package/dist/usage-DsYUqIUM.js.map +1 -0
  202. package/dist/view/index.d.ts +645 -0
  203. package/dist/view/index.js +8 -0
  204. package/dist/views-BrHOv6wg.js +220 -0
  205. package/dist/views-BrHOv6wg.js.map +1 -0
  206. package/dist/views-CwCWLWqo.js +3 -0
  207. package/dist/worker-entry-DnYkANPG.js +506 -0
  208. package/dist/worker-entry-DnYkANPG.js.map +1 -0
  209. package/dist/workspace-C2S0Z6gl.js +327 -0
  210. package/dist/workspace-C2S0Z6gl.js.map +1 -0
  211. package/dist/workspace-CyoXG6CY.js +2 -0
  212. package/docker/Dockerfile +19 -0
  213. package/package.json +79 -0
@@ -0,0 +1,1043 @@
1
+ import { t as UsageError } from "./errors-CSxfJ4qu.js";
2
+ import { a as fromCwd, l as sha256 } from "./ids-Bp6ut7ZD.js";
3
+ import { a as isFakeJudge, c as judgeRoute } from "./judge-D8Uf4dA9.js";
4
+ import { n as clip } from "./text-gEcKjAyp.js";
5
+ import { t as ANALYSIS_PROMPT_VERSION } from "./analysis-CxSCt1YY.js";
6
+ import { H as compare } from "./model-CDL98xue.js";
7
+ import { t as runHeader } from "./header-DzFDoMCI.js";
8
+ import { a as commandSummary, i as checkCommand } from "./cli-spec-DKAu09Gi.js";
9
+ import { a as parseRecord, f as scratchDir } from "./files-D2-qn_Dl.js";
10
+ import { c as resolveRun, s as pickBaseline, t as RefError } from "./views-BrHOv6wg.js";
11
+ import { c as lines, n as allRuns, t as CommandError, u as reachability } from "./commands-CF8mOfiu.js";
12
+ import { s as openHistoryDb } from "./db-DxDzMhtB.js";
13
+ import { t as author } from "./provenance-Dfb8YY5Y.js";
14
+ import { i as saveAnalysis, o as writeCached, r as readCached } from "./files-bF2fPuT9.js";
15
+ import { basename, join } from "node:path";
16
+ import { readFile } from "node:fs/promises";
17
+ import { z } from "zod";
18
+ import { Output, generateText } from "ai";
19
+ //#region src/analysis/digest.ts
20
+ const RULES = [
21
+ "Pass-rate lift is an effect only when its 95% interval (Newcombe) excludes 0; otherwise it is within noise, whatever the point estimate.",
22
+ "Effort (tool calls, agent time) is compared with an exact Mann-Whitney test: \"fewer\"/\"more\" at p < 0.05; \"insufficient\" when the attempt counts can never reach it (2 vs 2 cannot); pooled across evals it is an effect only when at least 2/3 of strata agree.",
23
+ "Each factor claim has a tier: controlled (two cells in one run that differ in one factor), pooled (the same contrast over comparable runs, and across evals), observed (a change between runs, which is weaker; \"confounded\" when several parts changed), not_measured (one level only).",
24
+ `A cell with mixed outcomes is flaky only with at least 6 counted attempts over comparable runs; below that it is "insufficient", not flaky.`,
25
+ "Drift marked noise changed only an agent CLI's own runtime state (e.g. ~/.cursor/cli-config.json): not a real change, and those runs stay comparable.",
26
+ "An eval where every cell passes at least 95% is saturated: pass rate cannot tell cells apart there, effort can.",
27
+ "Judge verdicts carry no recorded reason; an explanation of a failed verdict is your reading of the cited output, and must say so."
28
+ ];
29
+ const pts = (x) => x === null || x === void 0 ? null : `${x >= 0 ? "+" : "−"}${Math.round(Math.abs(x) * 100)}`;
30
+ const liftText = (points, lo, hi) => `${pts(points)} pts (95% ${pts(lo)}…${pts(hi)})`;
31
+ /** Short tags for the evals in scope: the file's base name, numbered when a file has several. */
32
+ function evalTags(evals) {
33
+ const tags = /* @__PURE__ */ new Map();
34
+ const bases = evals.map((e) => basename(e.file).replace(/\.(?:eval|es)\.ts$|\.[jt]s$/, ""));
35
+ evals.forEach((e, i) => {
36
+ const b = bases[i];
37
+ const same = bases.filter((x) => x === b).length;
38
+ tags.set(e.key, same > 1 ? `${b}-${bases.slice(0, i + 1).filter((x) => x === b).length}` : b);
39
+ });
40
+ return tags;
41
+ }
42
+ /** A cell's tag within its eval: its variant or `control`, prefixed by harness when there are several. */
43
+ function cellTags(e) {
44
+ const harnesses = [...new Set(e.cells.map((c) => c.harness))];
45
+ const kinds = [...new Set(e.cells.map((c) => c.harnessKind))];
46
+ const prefix = (c) => harnesses.length < 2 ? "" : `${kinds.length === harnesses.length ? c.harnessKind : c.harness}:`;
47
+ return new Map(e.cells.map((c) => [c.key, `${prefix(c)}${c.variant ?? "control"}`]));
48
+ }
49
+ const counted = (a) => a.outcome === "pass" || a.outcome === "fail";
50
+ async function buildDigest(input) {
51
+ const { head, base, db } = input;
52
+ const runs = base ? [base, head] : [head];
53
+ const short = new Map(runs.map((r, i) => [r.record.runId, `r${i + 1}`]));
54
+ const needle = input.evalFilter?.toLowerCase() ?? null;
55
+ const inScope = (e) => !needle || e.id.toLowerCase().includes(needle);
56
+ const scopeEvals = head.record.evals.filter(inScope);
57
+ const tags = evalTags(scopeEvals);
58
+ const evalKeys = new Set(scopeEvals.map((e) => e.key));
59
+ const tagOf = (key) => tags.get(key) ?? key;
60
+ const runIds = runs.map((r) => r.record.runId);
61
+ const attempts = {};
62
+ const idOf = /* @__PURE__ */ new Map();
63
+ const cellTagOf = /* @__PURE__ */ new Map();
64
+ for (const r of runs) for (const e of r.record.evals) {
65
+ if (!evalKeys.has(e.key)) continue;
66
+ const cells = cellTags(e);
67
+ for (const [key, tag] of cells) cellTagOf.set(`${r.record.runId}\0${e.key}\0${key}`, tag);
68
+ for (const a of e.attempts) {
69
+ const id = `${short.get(r.record.runId)}/${tagOf(e.key)}/${cells.get(a.cell)}#${a.n}`;
70
+ idOf.set(`${r.record.runId}\0${a.key}`, id);
71
+ const judge = a.judgements[0];
72
+ const any = a.files.transcript ?? a.files.judgeInput ?? a.checks.find((c) => c.log)?.log;
73
+ attempts[id] = {
74
+ run: short.get(r.record.runId),
75
+ runId: r.record.runId,
76
+ key: a.key,
77
+ eval: tagOf(e.key),
78
+ evalSlug: e.slug,
79
+ cell: cells.get(a.cell),
80
+ attempt: a.id,
81
+ outcome: a.outcome,
82
+ tools: a.activity.tools,
83
+ agentS: Math.round(a.timings.agentMs / 1e3),
84
+ ...judge ? { judge: Math.round(judge.score * 100) / 100 } : {},
85
+ checks: a.checks.length,
86
+ artifacts: any ? `${r.record.runId}/${any.path.split("/").slice(0, 3).join("/")}` : null
87
+ };
88
+ }
89
+ }
90
+ const cellTag = (runId, evalKey, cell) => cellTagOf.get(`${runId}\0${evalKey}\0${cell}`) ?? cell;
91
+ const facts = [];
92
+ const fact = (kind, body) => facts.push({
93
+ id: `F${facts.length + 1}`,
94
+ kind,
95
+ ...body
96
+ });
97
+ const q = (sql) => db.query(sql).rows;
98
+ const list = (values) => values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
99
+ const runList = list(runIds);
100
+ const evalList = list([...evalKeys]);
101
+ for (const r of runs) for (const e of r.record.evals) {
102
+ if (!evalKeys.has(e.key)) continue;
103
+ fact("pass-rate", {
104
+ run: short.get(r.record.runId),
105
+ eval: tagOf(e.key),
106
+ cells: e.cells.map((c) => ({
107
+ cell: cellTag(r.record.runId, e.key, c.key),
108
+ pass: `${c.pass}/${c.pass + c.fail}`,
109
+ ...c.infraError ? { infraError: c.infraError } : {},
110
+ gated: c.gate !== "none",
111
+ ...c.lift ? {
112
+ lift: liftText(c.lift.points, c.lift.interval.lo, c.lift.interval.hi),
113
+ verdict: c.lift.withinNoise ? "within noise" : "effect"
114
+ } : {}
115
+ }))
116
+ });
117
+ }
118
+ const effort = q(`SELECT * FROM effort_m WHERE run_id IN (${runList}) AND eval_key IN (${evalList}) ORDER BY run_id, rowid`);
119
+ const effortGroups = /* @__PURE__ */ new Map();
120
+ for (const row of effort) {
121
+ const id = `${row.run_id}\0${row.eval_key}`;
122
+ effortGroups.set(id, [...effortGroups.get(id) ?? [], row]);
123
+ }
124
+ for (const rows of effortGroups.values()) {
125
+ const first = rows[0];
126
+ const runId = first.run_id;
127
+ fact("effort", {
128
+ run: short.get(runId),
129
+ eval: tagOf(first.eval_key),
130
+ cells: rows.map((row) => ({
131
+ cell: cellTag(runId, row.eval_key, row.cell_key),
132
+ n: row.n,
133
+ medianToolCalls: row.tools_median,
134
+ medianAgentS: row.agent_s_median,
135
+ ...row.control_key ? { vsControl: {
136
+ toolCalls: `${row.tools_median} vs ${row.control_tools_median} (×${Number(row.tools_ratio).toFixed(2)})`,
137
+ agentS: `${row.agent_s_median} vs ${row.control_agent_s_median}`,
138
+ p: row.tools_p,
139
+ verdict: row.verdict
140
+ } } : {}
141
+ }))
142
+ });
143
+ }
144
+ const harnesses = [...new Set(runs.flatMap((r) => r.record.stack.harnesses.map((h) => h.id)))];
145
+ const factors = q(`SELECT * FROM factor_effects WHERE
146
+ (tier = 'pooled' AND (eval_key IS NULL OR eval_key IN (${evalList})))
147
+ OR (tier = 'controlled' AND factor != 'environment' AND run_id IN (${runList}) AND eval_key IN (${evalList}))
148
+ OR (tier = 'observed' AND run_id IN (${runList}) AND eval_key IN (${evalList}))
149
+ OR tier = 'not_measured'
150
+ ORDER BY CASE tier WHEN 'pooled' THEN 0 WHEN 'controlled' THEN 1 WHEN 'observed' THEN 2 ELSE 3 END,
151
+ factor, level, eval_key`).filter((f) => f.tier !== "pooled" || f.factor !== "environment" || harnesses.includes(f.context));
152
+ for (const f of factors) {
153
+ if (f.tier === "not_measured") {
154
+ fact("factor", {
155
+ tier: f.tier,
156
+ factor: f.factor,
157
+ level: f.level,
158
+ note: f.note
159
+ });
160
+ continue;
161
+ }
162
+ fact("factor", {
163
+ tier: f.tier,
164
+ factor: f.factor,
165
+ level: f.tier === "observed" ? short.get(f.level) ?? f.level : f.level,
166
+ baseline: f.tier === "observed" ? short.get(f.baseline) ?? f.baseline : f.baseline,
167
+ ...f.tier === "observed" ? {} : { context: f.context },
168
+ eval: f.eval_key ? tagOf(f.eval_key) : "all evals",
169
+ ...f.run_id && f.tier !== "observed" ? { run: short.get(f.run_id) } : {},
170
+ ...f.strata && f.strata > 1 ? { strata: f.strata } : {},
171
+ ...f.runs ? { runs: f.runs } : {},
172
+ passRate: `${f.variant_pass}/${f.variant_counted} vs ${f.control_pass}/${f.control_counted}`,
173
+ lift: liftText(f.points, f.lo, f.hi),
174
+ passVerdict: f.pass_verdict,
175
+ toolCalls: f.tools_ratios,
176
+ toolsP: f.tools_p,
177
+ effortVerdict: f.effort_verdict,
178
+ ...f.strata && f.strata > 1 ? {
179
+ consistentStrata: `${f.consistent}/${f.strata}`,
180
+ separatedStrata: `${f.separated}/${f.strata}`
181
+ } : { separated: f.separated === 1 },
182
+ ...f.note ? { note: f.note } : {}
183
+ });
184
+ }
185
+ const reads = q(`SELECT run_id, eval_key, cell_key, skill, COUNT(*) AS attempts, SUM(read_line IS NOT NULL) AS read,
186
+ GROUP_CONCAT(DISTINCT read_line) AS lines
187
+ FROM skill_reads WHERE run_id IN (${runList}) AND eval_key IN (${evalList})
188
+ GROUP BY run_id, eval_key, cell_key, skill ORDER BY run_id, eval_key, cell_key, skill`);
189
+ const byCell = /* @__PURE__ */ new Map();
190
+ for (const r of reads) {
191
+ const cell = cellTag(r.run_id, r.eval_key, r.cell_key);
192
+ const id = `${cell}\0${r.skill}`;
193
+ const entry = byCell.get(id) ?? {
194
+ cell,
195
+ skill: r.skill,
196
+ attempts: 0,
197
+ read: 0,
198
+ lines: /* @__PURE__ */ new Set()
199
+ };
200
+ entry.attempts += r.attempts;
201
+ entry.read += r.read;
202
+ for (const l of String(r.lines ?? "").split(",").filter(Boolean)) entry.lines.add(Number(l));
203
+ byCell.set(id, entry);
204
+ }
205
+ if (byCell.size) fact("skill-reads", {
206
+ note: "Attempts that read their placed skill (a tool call on skills/<name>/), and the transcript lines of the first read.",
207
+ cells: [...byCell.values()].map((c) => ({
208
+ cell: c.cell,
209
+ skill: c.skill,
210
+ read: `${c.read}/${c.attempts}`,
211
+ firstReadAtLines: [...c.lines].sort((a, b) => a - b)
212
+ }))
213
+ });
214
+ const pooled = /* @__PURE__ */ new Map();
215
+ for (const r of runs) for (const e of r.record.evals) {
216
+ if (!evalKeys.has(e.key)) continue;
217
+ const p = pooled.get(e.key) ?? {
218
+ pass: 0,
219
+ counted: 0,
220
+ cells: /* @__PURE__ */ new Map()
221
+ };
222
+ for (const c of e.cells) {
223
+ const [cp, cn] = p.cells.get(c.key) ?? [0, 0];
224
+ p.cells.set(c.key, [cp + c.pass, cn + c.pass + c.fail]);
225
+ p.pass += c.pass;
226
+ p.counted += c.pass + c.fail;
227
+ }
228
+ pooled.set(e.key, p);
229
+ }
230
+ const saturated = [...pooled].filter(([, p]) => [...p.cells.values()].every(([cp, cn]) => cn > 0 && cp / cn >= .95)).map(([key]) => tagOf(key));
231
+ fact("saturation", {
232
+ evals: [...pooled].map(([key, p]) => ({
233
+ eval: tagOf(key),
234
+ pass: `${p.pass}/${p.counted}`
235
+ })),
236
+ saturated
237
+ });
238
+ const judged = runs.flatMap((r) => r.record.evals.filter((e) => evalKeys.has(e.key)).flatMap((e) => e.attempts.flatMap((a) => a.judgements.map((j) => ({
239
+ r,
240
+ a,
241
+ j
242
+ })))));
243
+ if (judged.length) fact("judge", {
244
+ models: [...new Set(judged.map(({ j }) => j.resolvedModel ?? j.model))],
245
+ threshold: [...new Set(judged.map(({ j }) => j.threshold))],
246
+ verdicts: judged.length,
247
+ pass: judged.filter(({ j }) => j.verdict === "pass").length,
248
+ scoresAtLeast098: judged.filter(({ j }) => j.score >= .98).length,
249
+ nearThreshold: judged.filter(({ j }) => Math.abs(j.score - j.threshold) <= .1).length,
250
+ failures: judged.filter(({ j }) => j.verdict === "fail").map(({ r, a, j }) => ({
251
+ attempt: idOf.get(`${r.record.runId}\0${a.key}`),
252
+ score: j.score
253
+ })),
254
+ reasons: "not recorded"
255
+ });
256
+ const failures = runs.slice().reverse().flatMap((r) => r.record.evals.filter((e) => evalKeys.has(e.key)).flatMap((e) => e.attempts.filter((a) => a.outcome === "fail" || a.outcome === "infra_error").map((a) => ({
257
+ r,
258
+ e,
259
+ a
260
+ }))));
261
+ for (const { r, e, a } of failures.slice(0, 8)) {
262
+ const j = a.judgements.find((x) => x.verdict === "fail");
263
+ fact("failure", {
264
+ attempt: idOf.get(`${r.record.runId}\0${a.key}`),
265
+ outcome: a.outcome,
266
+ errorKind: a.error?.kind ?? null,
267
+ message: a.error ? clip(a.error.message.split("\n")[0] ?? "", 300) : null,
268
+ gated: e.cells.find((c) => c.key === a.cell)?.gate !== "none",
269
+ ...j ? { judge: {
270
+ question: j.question,
271
+ score: j.score,
272
+ threshold: j.threshold
273
+ } } : {},
274
+ failingChecks: a.checks.map((c, i) => ({
275
+ check: i + 1,
276
+ exit: c.exitCode,
277
+ command: c.command
278
+ })).filter((c) => c.exit !== 0)
279
+ });
280
+ }
281
+ for (const f of q(`SELECT * FROM flaky_cells WHERE eval_key IN (${evalList})`)) {
282
+ const cell = cellTag(head.record.runId, f.eval_key, f.cell_key);
283
+ fact("flaky", {
284
+ eval: tagOf(f.eval_key),
285
+ cell,
286
+ pass: `${f.pass}/${f.counted}`,
287
+ runs: f.runs,
288
+ wilson95: `${Math.round(f.lo * 100)}…${Math.round(f.hi * 100)}%`,
289
+ verdict: f.verdict,
290
+ cause: f.cause
291
+ });
292
+ }
293
+ if (base) {
294
+ const drift = q(`SELECT changed, changed_paths, noise, real_changed, COUNT(*) AS cells FROM drift
295
+ WHERE run_id = '${head.record.runId}' AND prev_run_id = '${base.record.runId}' AND eval_key IN (${evalList})
296
+ GROUP BY changed, changed_paths, noise, real_changed`);
297
+ for (const d of drift) fact("drift", {
298
+ from: "r1",
299
+ to: "r2",
300
+ cells: d.cells,
301
+ changed: d.changed,
302
+ ...d.changed_paths ? { paths: String(d.changed_paths).split(", ") } : {},
303
+ verdict: d.noise ? "noise: agent runtime state only" : `real change: ${d.real_changed}`
304
+ });
305
+ fact("compare", {
306
+ base: "r1",
307
+ head: "r2",
308
+ rows: compare(runHeader(base.record, true), runHeader(head.record, true)).filter((row) => scopeEvals.some((e) => e.id === row.id)).map((row) => ({
309
+ eval: tagOf(scopeEvals.find((e) => e.id === row.id)?.key ?? row.id),
310
+ status: row.status,
311
+ ...row.changes?.length ? { changes: row.changes } : {}
312
+ }))
313
+ });
314
+ }
315
+ const evidence = [];
316
+ const add = (ref, why, lines, priority) => {
317
+ if (lines.length) evidence.push({
318
+ id: `E${evidence.length + 1}`,
319
+ ref,
320
+ why,
321
+ lines,
322
+ priority
323
+ });
324
+ };
325
+ const runOf = new Map(runs.map((r) => [r.record.runId, r]));
326
+ const transcriptOf = async (runId, a) => {
327
+ const r = runOf.get(runId);
328
+ if (!r || !a.files.transcript) return null;
329
+ return readFile(join(r.dir, runId, ...a.files.transcript.path.split("/")), "utf8").catch(() => null);
330
+ };
331
+ const lastText = async (runId, a) => {
332
+ const text = await transcriptOf(runId, a);
333
+ if (!text) return a.turns.at(-1)?.text ? [`final: ${clip(a.turns.at(-1)?.text ?? "", 1400)}`] : [];
334
+ const lines = text.split("\n");
335
+ for (let i = lines.length - 1; i >= 0; i--) try {
336
+ const event = JSON.parse(lines[i]);
337
+ if (event.type === "text") return [`L${i + 1} text: ${clip(String(event.text), 1400)}`];
338
+ } catch {}
339
+ return [];
340
+ };
341
+ for (const { r, e, a } of failures.slice(0, 3)) {
342
+ const id = idOf.get(`${r.record.runId}\0${a.key}`);
343
+ const lines = await lastText(r.record.runId, a);
344
+ const j = a.judgements.find((x) => x.verdict === "fail");
345
+ if (j) lines.push(`judge question: ${j.question}`, `judge score: ${j.score} (threshold ${j.threshold})`);
346
+ for (const [i, c] of a.checks.entries()) {
347
+ if (c.exitCode === 0 || !c.log) continue;
348
+ const log = await readFile(join(r.dir, r.record.runId, ...c.log.path.split("/")), "utf8").catch(() => "");
349
+ lines.push(`check ${i + 1} (exit ${c.exitCode}) ${c.command}:`, ...log.trimEnd().split("\n").slice(-15));
350
+ }
351
+ add(`attempt:${id}`, `a failing attempt: its final message${j ? " and the judge question it failed" : ""}`, lines, 1);
352
+ if (j) {
353
+ const twin = e.attempts.find((x) => x.cell === a.cell && x.outcome === "pass");
354
+ if (twin) add(`attempt:${idOf.get(`${r.record.runId}\0${twin.key}`)}`, `the passing twin of ${id} (same cell): its final message`, await lastText(r.record.runId, twin), 2);
355
+ }
356
+ }
357
+ const strip = (runId, a) => q(`SELECT line, tool, target FROM tool_calls WHERE run_id = '${runId}' AND attempt_key = '${a.key.replace(/'/g, "''")}' ORDER BY line LIMIT 20`).map((t) => `L${t.line} ${t.tool} ${clip(String(t.target), 90)}`);
358
+ const medianAttempt = (list) => [...list].sort((x, y) => x.activity.tools - y.activity.tools)[Math.floor((list.length - 1) / 2)];
359
+ let strips = 0;
360
+ for (const e of scopeEvals) {
361
+ if (strips >= 3) break;
362
+ const contrast = effort.find((row) => row.run_id === head.record.runId && row.eval_key === e.key && row.control_key && (row.verdict === "fewer" || row.verdict === "more" || row.tools_ratio !== null && (row.tools_ratio <= .5 || row.tools_ratio >= 2)));
363
+ if (!contrast) continue;
364
+ strips++;
365
+ for (const cell of [contrast.cell_key, contrast.control_key]) {
366
+ const a = medianAttempt(e.attempts.filter((x) => x.cell === cell && counted(x)));
367
+ if (!a) continue;
368
+ add(`tool-strip:${idOf.get(`${head.record.runId}\0${a.key}`)}`, `the median-effort attempt of ${cellTag(head.record.runId, e.key, cell)} on ${tagOf(e.key)}: its first tool calls`, strip(head.record.runId, a), 3);
369
+ }
370
+ }
371
+ const digest = {
372
+ digestVersion: 1,
373
+ scope: {
374
+ kind: "run",
375
+ head: short.get(head.record.runId),
376
+ base: base ? "r1" : null,
377
+ evals: input.evalFilter
378
+ },
379
+ runs: Object.fromEntries(runs.map((r) => [short.get(r.record.runId), {
380
+ runId: r.record.runId,
381
+ commit: r.record.git?.sha.slice(0, 7) ?? null,
382
+ branch: r.record.git?.branch ?? null,
383
+ status: r.record.status,
384
+ summary: `${r.record.summary.pass}/${r.record.summary.attempts} attempts passed (${r.record.summary.notGated} failed, not gated)`
385
+ }])),
386
+ setup: {
387
+ harnesses,
388
+ judge: `${head.record.stack.judge.model ?? "none"} threshold ${head.record.stack.judge.threshold}`,
389
+ environments: head.record.environments.map((env) => ({
390
+ name: env.name,
391
+ skills: env.skills.map((s) => s.name)
392
+ })),
393
+ evals: scopeEvals.map((e) => ({
394
+ tag: tagOf(e.key),
395
+ id: e.id,
396
+ task: clip(e.attempts[0]?.turns[0]?.prompt ?? "", 400)
397
+ }))
398
+ },
399
+ rules: RULES,
400
+ cli: commandSummary(),
401
+ instructions: input.instructions ?? null,
402
+ attempts,
403
+ facts,
404
+ evidence,
405
+ dropped: []
406
+ };
407
+ while (digestChars(digest) > input.maxChars && digest.evidence.length) {
408
+ const worst = digest.evidence.reduce((a, b) => b.priority >= a.priority ? b : a);
409
+ digest.evidence = digest.evidence.filter((x) => x !== worst);
410
+ digest.dropped.push(worst.id);
411
+ }
412
+ return digest;
413
+ }
414
+ const digestChars = (d) => JSON.stringify(d).length;
415
+ /** Content hash of the digest: the cache key's input side. */
416
+ const digestHash = (d) => sha256(JSON.stringify(d));
417
+ /** Every id a citation may name, and the transcript lines each evidence item holds. */
418
+ function citable(d) {
419
+ return {
420
+ ids: /* @__PURE__ */ new Set([
421
+ ...d.facts.map((f) => f.id),
422
+ ...d.evidence.map((e) => e.id),
423
+ ...Object.keys(d.attempts)
424
+ ]),
425
+ lines: new Map(d.evidence.map((e) => [e.id, new Set(e.lines.flatMap((l) => /^L(\d+) /.exec(l) ? [Number(/^L(\d+) /.exec(l)?.[1])] : []))]))
426
+ };
427
+ }
428
+ //#endregion
429
+ //#region src/analysis/schema.ts
430
+ const confidence = z.enum([
431
+ "high",
432
+ "medium",
433
+ "low"
434
+ ]);
435
+ const cites = z.array(z.string()).describe("Ids from the digest: F<n>, E<n>, E<n>:L<line>, an attempt id, or <attempt id>:check<n>");
436
+ const claim = z.object({
437
+ text: z.string(),
438
+ cites,
439
+ confidence,
440
+ quotes: z.array(z.string()).optional().describe("Exact spans copied from the cited evidence, if the claim quotes any")
441
+ });
442
+ const analysisSchema = z.object({
443
+ tldr: z.string().describe("2-3 sentences: what happened, and does the thing under test help"),
444
+ whatHappened: z.array(claim),
445
+ results: z.array(claim).describe("Pass rate and effort, each with its noise verdict"),
446
+ why: z.array(claim).describe("Causes: the thing under test, the eval itself, or reeval/infrastructure"),
447
+ anomalies: z.array(z.object({
448
+ text: z.string(),
449
+ kind: z.enum([
450
+ "es-bug",
451
+ "eval-design",
452
+ "judge",
453
+ "noise",
454
+ "other"
455
+ ]),
456
+ cites
457
+ })),
458
+ actions: z.array(z.object({
459
+ title: z.string(),
460
+ target: z.string().describe("The file, skill, eval or reeval setting to change"),
461
+ kind: z.enum([
462
+ "skill",
463
+ "docs",
464
+ "eval",
465
+ "judge",
466
+ "es",
467
+ "harness"
468
+ ]),
469
+ change: z.string(),
470
+ expectedEffect: z.string().describe("What should measurably change"),
471
+ verify: z.string().describe("One reeval command from the CLI list that would show the effect"),
472
+ impact: z.enum([
473
+ "high",
474
+ "medium",
475
+ "low"
476
+ ]),
477
+ effort: z.enum([
478
+ "low",
479
+ "medium",
480
+ "high"
481
+ ]),
482
+ cites
483
+ })).max(5),
484
+ confidence: z.object({
485
+ overall: confidence,
486
+ why: z.string()
487
+ })
488
+ });
489
+ const SYSTEM_PROMPT = `You analyse the results of a reeval run for the engineer who wrote the evals.
490
+ reeval runs coding agents in sandboxes. A compare eval runs a control cell and variant cells (with a skill, docs or MCP, say) and reports pass-rate lift with a 95% interval, and effort (tool calls, agent time) next to it.
491
+
492
+ You get a DIGEST: facts computed deterministically by reeval (ids F1...), evidence excerpts (ids E1...; transcript lines start with their line number L<n>), an attempts table (ids like r2/fix-config/control#1), the RULES reeval used for every verdict, and the CLI.
493
+
494
+ Rules:
495
+ - Every claim cites at least one id from the digest: F<n>, E<n>, E<n>:L<line> for a line the evidence shows, an attempt id, or <attempt id>:check<n>. Never cite anything that isn't in the digest.
496
+ - Take numbers from the facts. Don't compute new statistics beyond simple ratios of cited numbers.
497
+ - The verdicts in the facts are final: if a lift or effort verdict says within noise or insufficient, say so; never call it an effect. Name each factor claim's tier (controlled, pooled, observed).
498
+ - Say what can't be attributed (factors that are not_measured).
499
+ - Separate causes: the thing under test (skill, docs), the eval itself (task, checks, judge question), and reeval or infrastructure (bugs, fingerprints, harness).
500
+ - Explain a failed judge verdict by comparing the cited output with the judge question, and say it's your reading: judges record no reason.
501
+ - Actions: at most 5, each with a concrete target, the change, the expected measurable effect, and a verify command that is exactly one of the reeval commands in the CLI list, with only the flags listed for it. "No actions" is a valid answer when nothing is worth doing. No generic advice ("add more tests") without a concrete target.
502
+ - Quotes, if any, must be copied exactly from the cited evidence.
503
+ - Plain, direct English. No hype.`;
504
+ //#endregion
505
+ //#region src/analysis/model.ts
506
+ const isFake = (m) => typeof m === "object" && m !== null && m.kind === "fake-analysis";
507
+ /** Which model analyses: `analysis.model`, else the judge's when it's a language model. */
508
+ function analysisModelName(config) {
509
+ const model = config.analysis.model;
510
+ if (model !== void 0) return model;
511
+ const judge = config.judge.model;
512
+ if (typeof judge === "string" && !judge.startsWith("typesafe-ai/")) return judge;
513
+ throw new UsageError(`reeval analyze needs a language model. Set analysis: { model: 'google/gemini-3.8-flash' } in ${config.file}${typeof judge === "string" || isFakeJudge(judge) ? ` (the judge, ${isFakeJudge(judge) ? "a fake judge" : judge}, is an evaluation model: it scores yes/no questions and can't write an analysis)` : ""}, or run reeval analyze --context to hand the digest to your own coding agent.`);
514
+ }
515
+ /** Checks the model and its credentials before any work; throws a UsageError (exit 2). */
516
+ function resolveAnalysisModel(config) {
517
+ const model = analysisModelName(config);
518
+ if (isFake(model)) return {
519
+ name: "fake",
520
+ route: "fake",
521
+ fake: model
522
+ };
523
+ if (model.startsWith("typesafe-ai/")) throw new UsageError(`config.analysis.model "${model}" is an evaluation model: it scores yes/no questions and can't write an analysis. Use a language model, e.g. 'google/gemini-3.8-flash'.`);
524
+ try {
525
+ return {
526
+ name: model,
527
+ route: judgeRoute(model).kind
528
+ };
529
+ } catch (error) {
530
+ throw new UsageError(`${error.message.replace("judge model", "analysis model")} Or run reeval analyze --context, which needs no key.`);
531
+ }
532
+ }
533
+ /** A language model that answers with the fake's output, through the AI SDK's own parsing. */
534
+ function fakeModel(fake, digest) {
535
+ const output = typeof fake.output === "function" ? fake.output(digest) : fake.output;
536
+ const text = typeof output === "string" ? output : JSON.stringify(output);
537
+ return {
538
+ specificationVersion: "v4",
539
+ provider: "fake",
540
+ modelId: "fake-analysis",
541
+ supportedUrls: {},
542
+ doGenerate: async () => ({
543
+ content: [{
544
+ type: "text",
545
+ text
546
+ }],
547
+ finishReason: {
548
+ unified: "stop",
549
+ raw: "stop"
550
+ },
551
+ usage: {
552
+ inputTokens: {
553
+ total: 0,
554
+ noCache: 0,
555
+ cacheRead: 0,
556
+ cacheWrite: 0
557
+ },
558
+ outputTokens: {
559
+ total: 0,
560
+ text: 0,
561
+ reasoning: 0
562
+ }
563
+ },
564
+ warnings: [],
565
+ response: { modelId: "fake-analysis" }
566
+ }),
567
+ doStream: async () => {
568
+ throw new Error("the fake analysis model does not stream");
569
+ }
570
+ };
571
+ }
572
+ async function languageModel(model, digest) {
573
+ if (model.fake) return fakeModel(model.fake, digest);
574
+ if (model.route === "gateway") return model.name;
575
+ const provider = model.name.slice(0, model.name.indexOf("/"));
576
+ const id = model.name.slice(provider.length + 1);
577
+ if (provider === "google") {
578
+ const { google } = await import("@ai-sdk/google");
579
+ return google(id);
580
+ }
581
+ return model.name;
582
+ }
583
+ async function callAnalysisModel(model, digest, prompt, maxOutputTokens) {
584
+ const started = Date.now();
585
+ const result = await generateText({
586
+ model: await languageModel(model, digest),
587
+ system: SYSTEM_PROMPT,
588
+ prompt,
589
+ output: Output.object({
590
+ schema: analysisSchema,
591
+ name: "analysis"
592
+ }),
593
+ temperature: .2,
594
+ maxOutputTokens,
595
+ maxRetries: 3
596
+ });
597
+ return {
598
+ output: result.output,
599
+ resolvedModel: result.response?.modelId ?? null,
600
+ usage: {
601
+ inputTokens: result.usage?.inputTokens ?? null,
602
+ outputTokens: result.usage?.outputTokens ?? null
603
+ },
604
+ latencyMs: Date.now() - started
605
+ };
606
+ }
607
+ //#endregion
608
+ //#region src/analysis/render.ts
609
+ const cite = (cs) => cs.length ? ` ${cs.map((c) => `[${c}]`).join(" ")}` : "";
610
+ const note = (c) => [c.confidence === "high" ? "" : `${c.confidence} confidence`, c.unverifiedNumbers?.length ? `unverified: ${c.unverifiedNumbers.join(", ")}` : ""].filter(Boolean).join("; ");
611
+ function footer(doc) {
612
+ const v = doc.validation;
613
+ const tokens = doc.usage.inputTokens !== null ? `${doc.usage.inputTokens} in / ${doc.usage.outputTokens ?? "?"} out tokens · ` : "";
614
+ return `${doc.resolvedModel ?? doc.model} · ${doc.promptVersion} · digest v${doc.inputs.digestVersion} · ${tokens}${Math.round(doc.latencyMs / 1e3)}s · ${v.citations} citations checked, ${v.invalid.length} invalid, ${v.dropped} dropped · ${v.commands} commands checked, ${v.invalidCommands.length} rejected`;
615
+ }
616
+ /** The analysis as Markdown (`--md`, and the `.md` twin of a saved analysis). */
617
+ function renderMarkdown(doc) {
618
+ const a = doc.analysis;
619
+ const md = [
620
+ `## reeval analysis · ${doc.scope.head}${doc.scope.base ? ` vs ${doc.scope.base}` : ""}`,
621
+ "",
622
+ `**TL;DR.** ${a.tldr}`,
623
+ ""
624
+ ];
625
+ for (const [title, items] of [
626
+ ["What happened", a.whatHappened],
627
+ ["Results", a.results],
628
+ ["Why", a.why]
629
+ ]) {
630
+ if (!items.length) continue;
631
+ md.push(`### ${title}`, "");
632
+ for (const c of items) md.push(`- ${c.text}${cite(c.cites)}${note(c) ? ` *(${note(c)})*` : ""}`);
633
+ md.push("");
634
+ }
635
+ if (a.anomalies.length) {
636
+ md.push("### Anomalies", "");
637
+ for (const x of a.anomalies) md.push(`- **${x.kind}**: ${x.text}${cite(x.cites)}`);
638
+ md.push("");
639
+ }
640
+ md.push("### Suggested actions", "");
641
+ if (!a.actions.length) md.push("None: nothing here is worth acting on.");
642
+ a.actions.forEach((x, i) => {
643
+ md.push(`${i + 1}. **${x.title}** (${x.kind} · impact ${x.impact} · effort ${x.effort}) `, ` Target: \`${x.target}\`. ${x.change} `, ` Expect: ${x.expectedEffect} `, x.verify ? ` Verify: \`${x.verify}\`${cite(x.cites)}` : ` Verify: none (the suggested command isn't reeval's CLI: ${x.verifyProblem ?? "unknown"})${cite(x.cites)}`);
644
+ });
645
+ md.push("", `**Confidence: ${a.confidence.overall}.** ${a.confidence.why}`, "", `<sub>${footer(doc)}</sub>`, "");
646
+ return md.join("\n");
647
+ }
648
+ /** The analysis for a terminal: the same sections, plain. */
649
+ function renderText(doc) {
650
+ const a = doc.analysis;
651
+ const lines = [
652
+ `analysis ${doc.scope.head}${doc.scope.base ? ` vs ${doc.scope.base}` : ""}`,
653
+ "",
654
+ `TL;DR ${a.tldr}`
655
+ ];
656
+ for (const [title, items] of [
657
+ ["What happened", a.whatHappened],
658
+ ["Results", a.results],
659
+ ["Why", a.why]
660
+ ]) {
661
+ if (!items.length) continue;
662
+ lines.push("", title);
663
+ for (const c of items) lines.push(` - ${c.text}${cite(c.cites)}${note(c) ? ` (${note(c)})` : ""}`);
664
+ }
665
+ if (a.anomalies.length) {
666
+ lines.push("", "Anomalies");
667
+ for (const x of a.anomalies) lines.push(` - ${x.kind}: ${x.text}${cite(x.cites)}`);
668
+ }
669
+ lines.push("", "Suggested actions");
670
+ if (!a.actions.length) lines.push(" none: nothing here is worth acting on");
671
+ a.actions.forEach((x, i) => {
672
+ lines.push(` ${i + 1}. ${x.title} (${x.kind} · impact ${x.impact} · effort ${x.effort})${cite(x.cites)}`, ` target: ${x.target}. ${x.change}`, ` expect: ${x.expectedEffect}`, x.verify ? ` verify: ${x.verify}` : ` verify: none (${x.verifyProblem ?? "rejected"})`);
673
+ });
674
+ lines.push("", `Confidence: ${a.confidence.overall}. ${a.confidence.why}`, "", footer(doc));
675
+ return lines.join("\n");
676
+ }
677
+ //#endregion
678
+ //#region src/analysis/validate.ts
679
+ const CHECK = /^(.+):check(\d+)$/;
680
+ const LINE = /^(E\d+):L(\d+)$/;
681
+ const TRANSCRIPT = /^transcript:(.+)#L(\d+)$/;
682
+ const normal = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
683
+ const NUMBER = /(?<![\w.#/:-])[−-]?\d+(?:\.\d+)?(?![\w/])/g;
684
+ /** A digest's citation checker: whether an id is valid, and the text it names. */
685
+ function citations(digest) {
686
+ const { ids, lines } = citable(digest);
687
+ const factText = new Map(digest.facts.map((f) => [f.id, JSON.stringify(f)]));
688
+ const evidenceText = new Map(digest.evidence.map((e) => [e.id, e.lines.join("\n")]));
689
+ const evidenceOf = (attempt) => digest.evidence.filter((e) => e.ref.endsWith(`:${attempt}`)).map((e) => e.id);
690
+ /** The text a citation names, or null when it names nothing in the digest. */
691
+ const resolve = (cite) => {
692
+ const c = cite.trim().replace(/^\[|\]$/g, "");
693
+ if (ids.has(c)) return factText.get(c) ?? evidenceText.get(c) ?? JSON.stringify(digest.attempts[c]);
694
+ const line = LINE.exec(c);
695
+ if (line) {
696
+ const [, id, n] = line;
697
+ if (!lines.get(id)?.has(Number(n))) return null;
698
+ return digest.evidence.find((e) => e.id === id)?.lines.find((l) => l.startsWith(`L${n} `)) ?? null;
699
+ }
700
+ const transcript = TRANSCRIPT.exec(c);
701
+ if (transcript) {
702
+ const [, attempt, n] = transcript;
703
+ for (const id of evidenceOf(attempt)) if (lines.get(id)?.has(Number(n))) return resolve(`${id}:L${n}`);
704
+ return null;
705
+ }
706
+ const check = CHECK.exec(c);
707
+ if (check) {
708
+ const [, attempt, n] = check;
709
+ const a = digest.attempts[attempt];
710
+ return a && Number(n) >= 1 && Number(n) <= a.checks ? JSON.stringify(a) : null;
711
+ }
712
+ return null;
713
+ };
714
+ return { resolve };
715
+ }
716
+ const ANY_NUMBER = /[−-]?\d+(?:\.\d+)?/g;
717
+ const FRACTION = /(\d+)\/(\d+)/g;
718
+ /**
719
+ * Numbers in `text` that none of `sources` contains: within rounding, as a percentage, or as the
720
+ * percentage of a `pass/counted` fraction there.
721
+ */
722
+ function unverifiedNumbers(text, sources) {
723
+ const known = sources.flatMap((s) => [...[...s.matchAll(ANY_NUMBER)].map((m) => Number(m[0].replace("−", "-"))), ...[...s.matchAll(FRACTION)].flatMap((m) => Number(m[2]) ? [100 * Number(m[1]) / Number(m[2]), Number(m[2]) - Number(m[1])] : [])]);
724
+ const close = (n) => known.some((k) => [
725
+ k,
726
+ k * 100,
727
+ k / 100,
728
+ -k
729
+ ].some((v) => Math.abs(v - n) <= Math.max(.01, Math.abs(v) * .01) || Math.round(v) === n || Math.round(v * 10) / 10 === n));
730
+ return [...new Set([...text.matchAll(NUMBER)].map((m) => m[0]))].filter((raw) => {
731
+ const n = Number(raw.replace("−", "-"));
732
+ if (Number.isInteger(n) && Math.abs(n) <= 5) return false;
733
+ return !close(n);
734
+ });
735
+ }
736
+ function validateAnalysis(output, digest, tables, evals) {
737
+ const { resolve } = citations(digest);
738
+ const validation = {
739
+ citations: 0,
740
+ invalid: [],
741
+ dropped: 0,
742
+ commands: 0,
743
+ invalidCommands: [],
744
+ unverifiedNumbers: 0
745
+ };
746
+ /** The item's valid citations and their texts; null (and counted) when none is valid. */
747
+ const cited = (where, cites) => {
748
+ const good = [];
749
+ const texts = [];
750
+ for (const c of cites) {
751
+ validation.citations++;
752
+ const text = resolve(c);
753
+ if (text === null) validation.invalid.push(`${where}: ${c}`);
754
+ else {
755
+ good.push(c.trim().replace(/^\[|\]$/g, ""));
756
+ texts.push(text);
757
+ }
758
+ }
759
+ if (good.length === 0) {
760
+ validation.dropped++;
761
+ return null;
762
+ }
763
+ return {
764
+ good,
765
+ texts
766
+ };
767
+ };
768
+ const claims = (where, items) => items.flatMap((item, i) => {
769
+ const c = cited(`${where}[${i}]`, item.cites);
770
+ if (!c) return [];
771
+ const haystack = normal(c.texts.join("\n"));
772
+ const invented = (item.quotes ?? []).find((quote) => !haystack.includes(normal(quote)));
773
+ if (invented !== void 0) {
774
+ validation.invalid.push(`${where}[${i}]: "${invented.slice(0, 120)}" isn't in the cited evidence`);
775
+ validation.dropped++;
776
+ return [];
777
+ }
778
+ const numbers = unverifiedNumbers(item.text, [...c.texts, ...digest.rules]);
779
+ validation.unverifiedNumbers += numbers.length;
780
+ return [{
781
+ text: item.text,
782
+ cites: c.good,
783
+ confidence: item.confidence,
784
+ ...item.quotes?.length ? { quotes: item.quotes } : {},
785
+ ...numbers.length ? { unverifiedNumbers: numbers } : {}
786
+ }];
787
+ });
788
+ const whatHappened = claims("whatHappened", output.whatHappened);
789
+ const results = claims("results", output.results);
790
+ const why = claims("why", output.why);
791
+ const anomalies = output.anomalies.flatMap((a, i) => {
792
+ const c = cited(`anomalies[${i}]`, a.cites);
793
+ return c ? [{
794
+ text: a.text,
795
+ kind: a.kind,
796
+ cites: c.good
797
+ }] : [];
798
+ });
799
+ const actions = output.actions.flatMap((a, i) => {
800
+ const c = cited(`actions[${i}]`, a.cites);
801
+ if (!c) return [];
802
+ const verify = a.verify.trim().replace(/^`+|`+$/g, "");
803
+ validation.commands++;
804
+ const problem = verify ? checkCommand(verify, tables, evals) : "no command";
805
+ if (problem) validation.invalidCommands.push(`${verify}: ${problem}`);
806
+ return [{
807
+ title: a.title,
808
+ target: a.target,
809
+ kind: a.kind,
810
+ change: a.change,
811
+ expectedEffect: a.expectedEffect,
812
+ verify: problem ? null : verify,
813
+ ...problem ? { verifyProblem: `${verify}: ${problem}` } : {},
814
+ impact: a.impact,
815
+ effort: a.effort,
816
+ cites: c.good
817
+ }];
818
+ });
819
+ return {
820
+ body: {
821
+ tldr: output.tldr,
822
+ whatHappened,
823
+ results,
824
+ why,
825
+ anomalies,
826
+ actions,
827
+ confidence: output.confidence
828
+ },
829
+ validation
830
+ };
831
+ }
832
+ //#endregion
833
+ //#region src/analysis/command.ts
834
+ async function load(config, entry) {
835
+ const dir = entry.recorded ? config.history.dir : scratchDir(config.root);
836
+ const bytes = await readFile(join(dir, `${entry.runId}.json`));
837
+ return {
838
+ record: parseRecord(bytes, entry.runId),
839
+ dir,
840
+ bytes
841
+ };
842
+ }
843
+ /** The model's input: the digest as JSON, after the project's own instructions. */
844
+ const promptOf = (d) => `${d.instructions ? `PROJECT INSTRUCTIONS: ${d.instructions}\n\n` : ""}DIGEST:\n${JSON.stringify(d)}`;
845
+ /** `--context`: the same digest and rules, for the user's own coding agent. */
846
+ function contextMarkdown(d, head, base) {
847
+ return [
848
+ `# reeval analysis context: ${head}${base ? ` vs ${base}` : ""}`,
849
+ "",
850
+ "Analyse this reeval run for the engineer who wrote the evals. The digest below holds facts reeval computed (F…), evidence excerpts (E…, transcript lines as L<n>) and the attempts table. Cite those ids for every claim, keep to the verdicts in the facts, and suggest at most 5 concrete actions, each verified with a reeval command from the CLI list.",
851
+ "",
852
+ "## How reeval decided each verdict",
853
+ "",
854
+ ...RULES.map((r) => `- ${r}`),
855
+ "",
856
+ "## Instructions reeval gives its own analysis model",
857
+ "",
858
+ SYSTEM_PROMPT,
859
+ "",
860
+ "## reeval CLI (verify commands must be one of these)",
861
+ "",
862
+ ...commandSummary().map((c) => `- \`${c}\``),
863
+ "",
864
+ "## To dig further",
865
+ "",
866
+ "- `reeval query effort`, `reeval query factor_effects`, `reeval query skill_reads`, `reeval query flaky_cells`, `reeval query drift`",
867
+ `- \`reeval query "SELECT line, tool, target FROM tool_calls WHERE attempt_key = '<key>'"\`: an attempt's tool calls`,
868
+ "- `reeval show <run> --json`: the whole record; transcripts are under the run's artifact folder",
869
+ "",
870
+ "## Digest",
871
+ "",
872
+ "```json",
873
+ JSON.stringify(d, null, 1),
874
+ "```",
875
+ ""
876
+ ].join("\n");
877
+ }
878
+ function citedSummary(d, cites) {
879
+ const out = {};
880
+ for (const c of cites) {
881
+ const fact = d.facts.find((f) => f.id === c);
882
+ const evidence = d.evidence.find((e) => e.id === c || c.startsWith(`${e.id}:`));
883
+ const attempt = d.attempts[c.replace(/:check\d+$/, "")];
884
+ if (fact) {
885
+ const { id: _id, ...rest } = fact;
886
+ out[c] = clip(JSON.stringify(rest), 400);
887
+ } else if (evidence) {
888
+ const line = /:L(\d+)$/.exec(c);
889
+ out[c] = line ? clip(evidence.lines.find((l) => l.startsWith(`L${line[1]} `)) ?? evidence.ref, 400) : `${evidence.ref}: ${evidence.why}`;
890
+ } else if (attempt) out[c] = `${attempt.runId} · ${attempt.attempt} · ${attempt.outcome} · ${attempt.tools} tool calls`;
891
+ }
892
+ return out;
893
+ }
894
+ async function analyzeCommand(ctx, args) {
895
+ const { config, flags } = ctx;
896
+ if (flags.json && flags.md) throw new UsageError("reeval analyze takes --json or --md, not both");
897
+ if (flags.context && flags.save) throw new UsageError("reeval analyze --context makes no analysis to save; drop --save");
898
+ const entries = await allRuns(config);
899
+ let evalFilter = flags.eval ?? null;
900
+ let head;
901
+ try {
902
+ head = resolveRun(entries, args[0] ?? "latest");
903
+ } catch (error) {
904
+ const needle = args[0]?.toLowerCase();
905
+ const latest = entries.at(-1);
906
+ if (!(error instanceof RefError) || !needle || !latest?.evals.some((e) => e.id.toLowerCase().includes(needle))) throw error;
907
+ evalFilter ??= args[0];
908
+ head = latest;
909
+ }
910
+ let base;
911
+ if (flags.vs) base = resolveRun(entries, flags.vs);
912
+ else {
913
+ const reach = await reachability(config.root);
914
+ const from = head.git && (!reach || reach.has(head.git.sha)) ? head.git.sha : "HEAD";
915
+ const ancestry = await lines(config.root, [
916
+ "rev-list",
917
+ "--max-count=5000",
918
+ from
919
+ ]);
920
+ base = pickBaseline(entries.filter((e) => e.recorded && e.runId !== head.runId), head, ancestry);
921
+ }
922
+ if (base?.runId === head.runId) throw new UsageError("reeval analyze --vs names the run itself");
923
+ if (flags.save && !head.recorded) throw new CommandError(`${head.runId} isn't recorded, so there's no history folder to save into. Run reeval record ${head.runId} first, then reeval analyze ${head.runId} --save.`);
924
+ const model = flags.context || flags["dry-run"] ? null : resolveAnalysisModel(config);
925
+ const headRun = await load(config, head);
926
+ const baseRun = base ? await load(config, base) : null;
927
+ const db = await openHistoryDb(config.root, config.history.dir);
928
+ let digest;
929
+ let tables;
930
+ try {
931
+ digest = await buildDigest({
932
+ head: headRun,
933
+ base: baseRun,
934
+ evalFilter,
935
+ db,
936
+ maxChars: config.analysis.maxInputTokens * 4,
937
+ ...config.analysis.instructions ? { instructions: config.analysis.instructions } : {}
938
+ });
939
+ tables = db.schema().map((o) => o.name);
940
+ } finally {
941
+ db.close();
942
+ }
943
+ if (evalFilter && digest.setup.evals.length === 0) throw new UsageError(`No eval in ${head.runId} matches "${evalFilter}"`);
944
+ if (flags.context) {
945
+ ctx.out(contextMarkdown(digest, head.runId, base?.runId ?? null));
946
+ return 0;
947
+ }
948
+ const modelName = (() => {
949
+ try {
950
+ const m = analysisModelName(config);
951
+ return typeof m === "string" ? m : "fake";
952
+ } catch {
953
+ return "none";
954
+ }
955
+ })();
956
+ const hash = digestHash(digest);
957
+ const scope = {
958
+ kind: "run",
959
+ head: head.runId,
960
+ base: base?.runId ?? null,
961
+ evals: evalFilter
962
+ };
963
+ const key = sha256(JSON.stringify({
964
+ scope,
965
+ hash,
966
+ prompt: ANALYSIS_PROMPT_VERSION,
967
+ model: modelName
968
+ })).replace(/^sha256:/, "");
969
+ const cached = flags["no-cache"] ? null : await readCached(config.root, key);
970
+ if (flags["dry-run"]) {
971
+ const chars = digestChars(digest);
972
+ ctx.out([
973
+ `digest ${head.runId}${base ? ` vs ${base.runId}` : ""}: ${digest.facts.length} facts, ${digest.evidence.length} evidence items${digest.dropped.length ? ` (${digest.dropped.length} dropped to fit)` : ""}, ${Object.keys(digest.attempts).length} attempts`,
974
+ `size ${chars} characters ≈ ${Math.ceil(chars / 4)} tokens (cap ${config.analysis.maxInputTokens} in, ${config.analysis.maxOutputTokens} out)`,
975
+ `model ${modelName} · ${ANALYSIS_PROMPT_VERSION} · cache ${cached ? "hit" : "miss"} (.reeval/analysis/${key}.json)`
976
+ ].join("\n"));
977
+ return 0;
978
+ }
979
+ let doc;
980
+ if (cached) {
981
+ doc = cached;
982
+ ctx.err(`cached analysis from ${cached.createdAt} (.reeval/analysis/${key}.json); --no-cache calls the model again`);
983
+ } else {
984
+ const resolved = model ?? resolveAnalysisModel(config);
985
+ ctx.err(`analysing ${head.runId}${base ? ` vs ${base.runId}` : ""} with ${resolved.name}…`);
986
+ const answer = await callAnalysisModel(resolved, digest, promptOf(digest), config.analysis.maxOutputTokens);
987
+ const evals = headRun.record.evals.map((e) => ({
988
+ file: e.file,
989
+ name: [...e.group, e.name].join(" "),
990
+ tags: e.tags,
991
+ cells: e.cells.map((c) => c.key)
992
+ }));
993
+ const { body, validation } = validateAnalysis(answer.output, digest, tables, evals);
994
+ const cites = [
995
+ ...body.whatHappened,
996
+ ...body.results,
997
+ ...body.why,
998
+ ...body.anomalies,
999
+ ...body.actions
1000
+ ].flatMap((x) => x.cites);
1001
+ doc = {
1002
+ kind: "es-analysis",
1003
+ schemaVersion: 1,
1004
+ scope,
1005
+ anchor: head.runId,
1006
+ inputs: {
1007
+ records: [baseRun, headRun].flatMap((r) => r ? [{
1008
+ runId: r.record.runId,
1009
+ sha256: sha256(r.bytes)
1010
+ }] : []),
1011
+ digestVersion: digest.digestVersion,
1012
+ digestHash: hash
1013
+ },
1014
+ promptVersion: ANALYSIS_PROMPT_VERSION,
1015
+ model: resolved.name,
1016
+ resolvedModel: answer.resolvedModel,
1017
+ route: resolved.route,
1018
+ usage: answer.usage,
1019
+ latencyMs: answer.latencyMs,
1020
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1021
+ author: await author(config.root),
1022
+ validation,
1023
+ analysis: body,
1024
+ attempts: Object.fromEntries(Object.entries(digest.attempts).map(([id, a]) => [id, {
1025
+ runId: a.runId,
1026
+ evalSlug: a.evalSlug,
1027
+ attempt: a.attempt
1028
+ }])),
1029
+ cited: citedSummary(digest, [...new Set(cites)])
1030
+ };
1031
+ await writeCached(config.root, key, doc);
1032
+ }
1033
+ if (flags.save) {
1034
+ const saved = await saveAnalysis(config.history.dir, doc, key, renderMarkdown(doc));
1035
+ ctx.err(`saved ${saved.map((p) => fromCwd(p)).join(" and ")}. Commit them with git.`);
1036
+ }
1037
+ ctx.out(flags.json ? JSON.stringify(doc, null, 2) : flags.md ? renderMarkdown(doc) : renderText(doc));
1038
+ return 0;
1039
+ }
1040
+ //#endregion
1041
+ export { analyzeCommand };
1042
+
1043
+ //# sourceMappingURL=command-CEm8PZ1N.js.map