eval: 第 1 轮检验脚本(跑、杀进程、导出去来历、盲评编号、Jev 三问、数字与过程)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 13:59:54 -04:00
parent f98d664a16
commit a2a1392f01
12 changed files with 1259 additions and 1 deletions
+172
View File
@@ -0,0 +1,172 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import type { FrozenProtocol } from "../../src/materials/index.ts";
import { finalSubmissions, ofKind, readLedger, stages } from "../lib/ledger.ts";
import { readTimeline } from "../lib/timeline.ts";
// 第 1 轮的数字和过程(任务书 14 第 3.2 节第 4 步),只看我们的运行;另查程序部分(第 3.4 节):
// 阶段开关成对、冻结协议写在回应阶段关闭之后、最后停下的原因是"实现还没写"。不花钱,只读账本和 timeline.jsonl。
// node eval/round-one/stats.ts <检验目录>...(run.ts、kill.ts 写出的目录:study/、timeline.jsonl)
// 每个目录写一份 stats.json,并打印摘要。
/** 已知最好和基线分数(任务书 14 第 3.2 节第 4 步给的数),预测超出这两头的逐条列出。 */
const BEST_KNOWN = 2.635983;
const BASELINE = 0.9598;
/** flow 写停下事件用的事件名(src/flow/next-step.ts 的 EVENTS.halted;flow 只导出 runStudy,这里照抄)。 */
const HALTED = "halted";
const EXPECTED_STOP = "实现还没写";
const dirs = process.argv.slice(2);
if (dirs.length === 0) throw new Error("要写一个或几个检验目录(run.ts、kill.ts 写出的)");
type Range3 = { q1: number; q5: number; q9: number };
const measured = (m: { status: string; value?: number }) => (m.status === "measured" ? m.value! : null);
const sum = (xs: Array<number | null>) => ({ total: xs.reduce<number>((a, x) => a + (x ?? 0), 0), unmeasured: xs.filter((x) => x === null).length });
function statsOf(dir: string) {
const records = readLedger(join(dir, "study"));
const timelineFile = join(dir, "timeline.jsonl");
if (!existsSync(timelineFile)) throw new Error(`${timelineFile} 不存在:墙钟时间要靠 run.ts、kill.ts 记下的时刻`);
const seen = new Map(readTimeline(timelineFile).map((r) => [r.seq, r.at]));
const at = (seq: number) => {
const t = seen.get(seq);
if (t === undefined) throw new Error(`timeline.jsonl 里没有序号 ${seq} 的时刻`);
return t;
};
const allStages = stages(records);
const opens = new Map(ofKind(records, "stageOpen").map((r) => [r.content.stageId, r.seq]));
const closes = new Map(ofKind(records, "stageClose").map((r) => [r.content.stageId, r.seq]));
const calls = ofKind(records, "callStarted");
const ends = new Map(ofKind(records, "callEnded").map((r) => [r.content.call, r.content]));
const usage = ofKind(records, "callUsage");
const rejections = ofKind(records, "rejection");
// ---- 每个阶段:调用、结局、墙钟、用量、退回 ----
const perStage = allStages.map((s) => {
const these = calls.filter((c) => c.content.stageId === s.stageId);
const ids = new Set(these.map((c) => c.content.call));
const endings: Record<string, number> = {};
for (const c of these) {
const e = ends.get(c.content.call);
const k = e === undefined ? "没有结束记录" : e.status === "failed" ? `failed:${e.reason}` : e.status;
endings[k] = (endings[k] ?? 0) + 1;
}
const u = usage.filter((x) => ids.has(x.content.call)).map((x) => x.content.usage);
const rej = rejections.filter((r) => r.content.stageId === s.stageId);
const byKind: Record<string, string[]> = {};
for (const r of rej) (byKind[r.content.kind] ??= []).push(`${r.content.author}${r.content.target === null ? "" : ` → ${r.content.target}`}:${r.content.reason}`);
const close = closes.get(s.stageId);
return {
stageId: s.stageId,
round: s.round,
stage: s.stage,
calls: these.length,
endings,
wallClockSeconds: close === undefined ? null : (at(close) - at(opens.get(s.stageId)!)) / 1000,
callWallSeconds: sum(these.map((c) => {
const e = ends.get(c.content.call);
return e === undefined ? null : measured(e.wallSeconds);
})),
tokens: {
input: sum(u.map((x) => measured(x.inputTokens))),
cacheRead: sum(u.map((x) => measured(x.cacheReadTokens))),
cacheWrite: sum(u.map((x) => measured(x.cacheWriteTokens))),
output: sum(u.map((x) => measured(x.outputTokens))),
cost: sum(u.map((x) => measured(x.cost))),
},
rejections: { count: rej.length, byKind },
};
});
// ---- 停下、交不出、卡住 ----
const events = ofKind(records, "event").map((e) => ({ seq: e.seq, ...e.content }));
const notSubmitted = calls.filter((c) => ends.get(c.content.call)?.status === "notSubmitted").map((c) => ({ call: c.content.call, end: ends.get(c.content.call) }));
const openCalls = calls.filter((c) => !ends.has(c.content.call)).map((c) => c.content.call);
const openStages = allStages.filter((s) => !s.closed).map((s) => s.stageId);
const outcome = existsSync(join(dir, "outcome.json")) ? JSON.parse(readFileSync(join(dir, "outcome.json"), "utf8")) : null;
// ---- 预测 ----
const finals = finalSubmissions(records);
const round1 = allStages.filter((s) => s.round === 1);
const stageId = (name: string) => {
const s = round1.find((x) => x.stage === name);
if (s === undefined) throw new Error(`第 1 轮没有 ${name} 阶段`);
return s.stageId;
};
const predictions = (name: string) =>
finals
.filter((f) => f.content.stageId === stageId(name) && f.content.kind === name)
.map((f) => ({ researcher: f.content.author, candidate: f.content.target!, range: (f.content.content as { range: Range3 }).range }));
const first = predictions("firstPrediction");
const second = predictions("secondPrediction");
const outOfRange = [...first.map((p) => ({ ...p, which: "第一次" })), ...second.map((p) => ({ ...p, which: "第二次" }))].flatMap((p) =>
(["q1", "q5", "q9"] as const)
.filter((q) => p.range[q] > BEST_KNOWN || p.range[q] < BASELINE)
.map((q) => ({ which: p.which, researcher: p.researcher, candidate: p.candidate, q, value: p.range[q], side: p.range[q] > BEST_KNOWN ? "超过已知最好" : "低于基线" })),
);
const changes = first.map((f) => {
const s = second.find((x) => x.researcher === f.researcher && x.candidate === f.candidate);
return {
researcher: f.researcher,
candidate: f.candidate,
first: f.range,
second: s?.range ?? null,
identical: s !== undefined && f.range.q1 === s.range.q1 && f.range.q5 === s.range.q5 && f.range.q9 === s.range.q9,
delta: s === undefined ? null : { q1: s.range.q1 - f.range.q1, q5: s.range.q5 - f.range.q5, q9: s.range.q9 - f.range.q9 },
};
});
// ---- 程序部分 ----
const unpaired = [...opens.keys()].filter((id) => !closes.has(id)).concat([...closes.keys()].filter((id) => !opens.has(id)));
const protocols = ofKind(records, "protocol").map((p) => {
const round = (p.content.protocol as unknown as FrozenProtocol).round;
const response = allStages.find((s) => s.round === round && s.stage === "response");
const close = response === undefined ? undefined : closes.get(response.stageId);
return { candidate: p.content.candidate, round, seq: p.seq, responseCloseSeq: close ?? null, after: close !== undefined && p.seq > close };
});
const lastHalt = events.filter((e) => e.event === HALTED).at(-1) ?? null;
const checks = {
"每个阶段开关成对": { pass: unpaired.length === 0, unpaired },
"冻结协议都写在回应阶段关闭之后": { pass: protocols.length > 0 && protocols.every((p) => p.after), protocols },
"最后停下原因是实现还没写": {
pass: lastHalt !== null && lastHalt.reason.includes(EXPECTED_STOP) && lastHalt.seq === records.at(-1)!.seq,
lastHalt,
lastRecord: { seq: records.at(-1)!.seq, kind: records.at(-1)!.kind },
},
};
const times = [...seen.values()];
return {
study: join(dir, "study"),
totalWallClockSeconds: (Math.max(...times) - Math.min(...times)) / 1000,
perStage,
events,
notSubmitted,
stuck: { openCalls, openStages },
outcome,
predictions: { bestKnown: BEST_KNOWN, baseline: BASELINE, outOfRange, changes },
checks,
};
}
for (const dir of dirs) {
const s = statsOf(dir);
writeFileSync(join(dir, "stats.json"), `${JSON.stringify(s, null, 2)}\n`);
const f = (x: number) => x.toFixed(0);
console.log(`\n== ${dir}(总墙钟 ${f(s.totalWallClockSeconds)} 秒)`);
for (const p of s.perStage) {
const rej = Object.entries(p.rejections.byKind).map(([k, v]) => `${k} ${v.length}`).join("、") || "无";
console.log(
`${p.stageId}:调用 ${p.calls}(${Object.entries(p.endings).map(([k, v]) => `${k} ${v}`).join(",")}),墙钟 ${p.wallClockSeconds === null ? "未关闭" : `${f(p.wallClockSeconds)} 秒`},` +
`输入 ${p.tokens.input.total}/输出 ${p.tokens.output.total} token,费用 ${p.tokens.cost.total.toFixed(2)}${p.tokens.cost.unmeasured > 0 ? `(${p.tokens.cost.unmeasured} 条测不到)` : ""},退回 ${rej}`,
);
}
console.log(`事件:${s.events.map((e) => `#${e.seq} ${e.event}:${e.reason}`).join(";")}`);
console.log(`交不出:${s.notSubmitted.length};卡住:未结束的调用 ${JSON.stringify(s.stuck.openCalls)},未关闭的阶段 ${JSON.stringify(s.stuck.openStages)}`);
console.log(`超出范围的预测:${s.predictions.outOfRange.map((o) => `${o.which} ${o.researcher}→${o.candidate} ${o.q}=${o.value}(${o.side})`).join(";") || "无"}`);
for (const c of s.predictions.changes) {
const r = (x: Range3 | null) => (x === null ? "没有" : `${x.q1}/${x.q5}/${x.q9}`);
console.log(` ${c.researcher}→${c.candidate}:第一次 ${r(c.first)},第二次 ${r(c.second)}${c.identical ? "(完全相同)" : ""}`);
}
for (const [k, v] of Object.entries(s.checks)) console.log(`${v.pass ? "通过" : "不通过"} ${k}`);
}