1#!/usr/bin/env node
2// @effect-diagnostics nodeBuiltinImport:off globalConsole:off
3/**
4 * Grades saved samples again with this checkout's cases and graders: `pnpm mcp-tool-use:regrade
5 * <results.jsonl>... [--cases <file>]` prints pass counts per case, old → new. Rows with an error
6 * are skipped. A sample that stopped early and passes now is "cut short": its turn never finished.
7 */
8import * as NodeFS from "node:fs";
9import * as NodeURL from "node:url";
10import * as NodeUtil from "node:util";
11import * as Schema from "effect/Schema";
12
13import { gradeSample, McpToolUseCase, pastedText, worldFilePaths } from "./run.ts";
14
15const { values, positionals } = NodeUtil.parseArgs({
16 allowPositionals: true,
17 options: { cases: { type: "string", multiple: true } },
18});
19const caseFiles =
20 values.cases ??
21 NodeFS.readdirSync(new URL("./cases", import.meta.url))
22 .filter((name) => name.endsWith(".ndjson"))
23 .map((name) => NodeURL.fileURLToPath(new URL(`./cases/${name}`, import.meta.url)));
24const decodeCase = Schema.decodeUnknownSync(Schema.fromJsonString(McpToolUseCase));
25const lines = (file: string) =>
26 NodeFS.readFileSync(file, "utf8")
27 .split("\n")
28 .filter((line) => line.trim() !== "");
29const cases = new Map(
30 caseFiles.flatMap(lines).map((line) => [decodeCase(line).id, decodeCase(line)]),
31);
32
33for (const file of positionals) {
34 const counts = new Map<string, { n: number; was: number; now: number; cut: number }>();
35 for (const line of lines(file)) {
36 const row = JSON.parse(line);
37 const testCase = cases.get(row.id);
38 if (testCase === undefined || row.error !== undefined) continue;
39 const failed = gradeSample(
40 testCase.expect,
41 row,
42 pastedText(testCase),
43 worldFilePaths(testCase),
44 ).filter((check) => !check.pass);
45 const cut = failed.length === 0 && row.stopReason === "failed_early";
46 const pass = failed.length === 0 && !cut;
47 const count = counts.get(row.id) ?? { n: 0, was: 0, now: 0, cut: 0 };
48 count.n++;
49 if (row.pass) count.was++;
50 if (pass) count.now++;
51 if (cut) count.cut++;
52 counts.set(row.id, count);
53 if (pass !== row.pass || cut)
54 console.log(
55 `${row.id}#${count.n} ${row.pass ? "pass" : "FAIL"} → ${cut ? "cut short" : pass ? "pass" : "FAIL"}${failed.length > 0 ? `: ${failed.map((check) => check.check.slice(0, 100)).join(" | ")}` : ""}`,
56 );
57 }
58 console.log(`\n${file}\n| case | was | now |\n| --- | --- | --- |`);
59 for (const [id, { n, was, now, cut }] of counts)
60 console.log(
61 `| ${id} | ${was}/${n} | ${now}/${n}${cut > 0 ? ` (${cut} cut short)` : ""}${was === now ? "" : " *"} |`,
62 );
63}