import { writeFileSync } from "node:fs"; import { pyDumps, pyRepr, readRun, type J } from "./py.ts"; // 任务书 15 第 4.1 节:把一次运行的第零轮和第 1 轮导出成一份纯文本,供子 Agent 按附录一问卷阅读。 // 逐行移植对照材料里的 extract.py,输出逐字节一致(含原脚本的写法:结尾不带换行; // 认领入口名是从调用记录的 JSON 文字里用正则抠的,名字里的引号、换行会带着 JSON 转义)。 // node eval/route/extract.ts <运行目录名> <输出文件> // 运行目录是 /home/iomgaa/inforsi-eval/<运行目录名>/study/ledger.sqlite,只读打开。 const [run, out] = process.argv.slice(2); if (run === undefined || out === undefined) throw new Error("用法:node eval/route/extract.ts <运行目录名> <输出文件>"); const rows = readRun(run); const L: string[] = []; const subs: J[] = rows.filter(([k]) => k === "submission").map(([, c]) => c); const decisions = (name: string): J[] => rows.filter(([k, c]) => k === "decision" && c.decision === name).map(([, c]) => c); L.push(`# 运行 ${run}\n`); L.push("## 第零轮 一、闭门写的入口(每人按自己写的先后)\n"); for (const s of subs) { if (s.kind !== "entry") continue; L.push(`### ${s.author}(第 ${s.version} 版)`); s.content.entries.forEach((e: J, i: number) => L.push(`#### 第 ${i + 1} 个:${e.name}\n${e.body}\n`)); } L.push("## 第零轮 二、讨论(按提交先后)\n"); for (const s of subs) { if (s.kind !== "discussion") continue; L.push(`### ${s.author}(${s.stageId},第 ${s.version} 版)\n${s.content.body ?? ""}\n`); if (s.content.entries?.length) { L.push("换成的新入口清单:"); s.content.entries.forEach((e: J, i: number) => L.push(`#### 新第 ${i + 1} 个:${e.name}\n${e.body}\n`)); } } // 认领时的最终入口编号:从第一条认领调用的记录(材料里的入口全文)里抠出来。 const names = new Map(); for (const [k, c] of rows) { if (k !== "callStarted") continue; const txt = pyDumps(c, false); if (!txt.includes("r0-claim")) continue; for (const m of txt.matchAll(/### 入口 (e-researcher-\d-\d+):(.*?)(作者 (researcher-\d))/g)) names.set(m[1]!, [m[2]!, m[3]!]); break; } L.push("## 第零轮 三、认领时的最终入口编号\n"); for (const [i, [n, a]] of names) L.push(`- ${i}(作者 ${a}):${n}`); L.push("\n## 第零轮 四、志愿与分配\n"); for (const s of subs) if (s.kind === "claim") L.push(`- ${s.author} 志愿:${s.content.choices.join(" > ")}`); for (const c of decisions("entries")) L.push("分配:" + pyDumps(c.output, false)); const props = subs.filter((s) => s.kind === "proposal"); if (props.length > 0) { L.push("\n## 第 1 轮 一、提案全文\n"); for (const s of props) { L.push(`### ${s.item}(作者 ${s.author})\n${s.content.body}\n`); L.push("因子:"); for (const f of s.content.factors) L.push(`- ${f.name}:${f.method}`); L.push(""); } for (const c of decisions("dedup")) L.push("判重:" + pyDumps(c.output, false)); for (const kind of ["firstPrediction", "secondPrediction"]) { L.push(`\n## 第 1 轮 ${kind} 三个数(作者→候选)`); for (const s of subs) if (s.kind === kind) L.push(`- ${s.author} → ${s.target}:${pyRepr(s.content.range)}`); } for (const c of decisions("batch")) { const merged = c.output.candidates.map((x: J) => [x.id, x.merged]); L.push("\n选批:入选 " + pyRepr(c.output.chosen) + ";合并范围 " + pyDumps(merged, true)); } L.push("\n## 附:第二次预测正文(看研究者如何判断风险)\n"); for (const s of subs) if (s.kind === "secondPrediction") L.push(`### ${s.author} → ${s.target}\n${s.content.body}\n`); } writeFileSync(out, L.join("\n"));