eval: 完整研究的运行、统计与审阅导出(任务书 17 第三块的检验代码)

- eval/full/run.ts:配置照 eval/round-zero/research.json 只换编号,真实跑到停下;
  launches/machine/timeline/outcome 照任务书 16 惯例;--kill-after-first-ablation-start
  在第一个消融运行确认启动后、结果落账前 SIGKILL 控制进程,恢复续跑到底,
  kill-report.json 写三条检查(同一容器不重跑、只扣一次、结果照常写入)。
- eval/lib/full.ts:账目核对 fullAccounts——逐轮走向(候选分数、采纳、选格、每格
  核验重做次数与扣次、结算、押的因子)加 12 项程序账目检查(阶段开关成对、
  扣次=确认启动数、余额为 0、解释冻结早于首格启动、每轮最多一条结算、解封在
  收尾关闭之后且覆盖全部选中候选、跑不起来/不跑的格零扣次、第 2 轮起基线链
  重算、停在"导出还没写"等)与 CPU/墙钟比(<0.9 列出并附同时段负载,只列事实)。
- eval/full/stats.ts、export.ts:统计输出与按轮按格的审阅材料导出。
- 不沿用 eval/forward/acceptance.ts 的"启动区间互不重叠"(这次 3 份同时跑)。
- tests/full-eval.test.ts 4 条:真实账本 + rules 真实计算走完一轮含消融的研究,
  撑几成与配合关系标签按方法.md 6.6 手算对照。npm run check 通过,npm test 476 条通过。
This commit is contained in:
2026-10-04 14:09:09 -04:00
parent 144b88480b
commit 0ae495f8e8
6 changed files with 1606 additions and 1 deletions
+273
View File
@@ -0,0 +1,273 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import {
Board,
cellId,
explanationId,
protocolMetrics,
splitCell,
type TaskMaterial,
} from "../../src/materials/index.ts";
import { ablationOutcome } from "../../src/submissions/index.ts";
import { checkoutVersion, compareVersions } from "../../src/workspace/index.ts";
import { jsonFile } from "../lib/forward.ts";
import { finalSubmissions, ofKind, readLedger } from "../lib/ledger.ts";
// 任务书 17 第 3.2 节研究质量评估的审阅材料:解释、选格决定、消融格(协议、位置表、代码与 diff、实现与核验往来)、
// 结算、收尾、下一轮提案。只导出事实和原文,不替审阅者下结论。
// node eval/full/export.ts <研究输出目录> <全新绝对审阅目录>
const [dir, output, ...extra] = process.argv.slice(2);
if (!dir || !output || extra.length || output !== resolve(output)) {
throw new Error(
"用法:node eval/full/export.ts <研究输出目录> <全新绝对审阅目录>",
);
}
mkdirSync(output); // 不覆盖以前的审阅材料。
const study = join(dir, "study");
const records = readLedger(study);
const board = new Board(records);
const repo = join(study, "repo.git");
const finals = finalSubmissions(records);
const roundOf = (stageId: string) => board.stage(stageId).round;
const roundIds = [
...new Set(ofKind(records, "decision").map((d) => d.content.round)),
]
.filter((r) => r >= 1)
.sort((a, b) => a - b);
const checkout = (version: string, path: string) => {
mkdirSync(path);
checkoutVersion(repo, version, path);
};
const json = (v: unknown) => `${JSON.stringify(v, null, 2)}\n`;
const index = [];
for (const round of roundIds) {
const roundDir = join(output, `round-${round}`);
mkdirSync(roundDir);
// 本轮提案(下一轮提案在 round-<r+1> 里,审阅"收尾→接力"时把两份对着读)。
const proposals = finals.filter(
(s) =>
s.content.kind === "proposal" &&
roundOf(s.content.stageId) === round,
);
jsonFile(join(roundDir, "proposals.json"), proposals);
writeFileSync(
join(roundDir, "proposals.txt"),
proposals
.map(
(s) =>
`${"=".repeat(20)} ${s.content.item}(${s.content.author},记录 ${s.seq})\n${json(s.content.content)}`,
)
.join("\n"),
);
// 解释(任务书 17 第 3.2 节第 1 样):作者、正文、每格三个数。
const explanations = finals.filter(
(s) =>
s.content.kind === "explanation" &&
roundOf(s.content.stageId) === round,
);
jsonFile(join(roundDir, "explanations.json"), explanations);
writeFileSync(
join(roundDir, "explanations.txt"),
explanations
.flatMap((s) =>
(
s.content.content as {
explanations: Array<{
body: string;
cells?: Array<{ factor: string; range: unknown }>;
}>;
}
).explanations.map(
(e, i) =>
`${"=".repeat(20)} ${explanationId(round, s.content.author, i + 1)}(${s.content.author} 的第 ${i + 1} 条,记录 ${s.seq})\n${e.body}\n三个数:${(e.cells ?? []).map((c) => `${c.factor} ${JSON.stringify(c.range)}`).join(";")}`,
),
)
.join("\n\n"),
);
// 选格决定全文(第 2 样):因子、合并范围、各组合的信息价值/成本/净值、选中与没选的原因都在这条决定里。
const cellsDecision =
ofKind(records, "decision").find(
(d) => d.content.decision === "cells" && d.content.round === round,
) ?? null;
const chosenCells = board.decision("cells", round)?.output.chosen ?? null;
jsonFile(join(roundDir, "cells-decision.json"), cellsDecision);
// 结算全文(第 4 样):结算记录和同一事务里押的因子决定。
const settlement =
ofKind(records, "settlement").find((s) => s.content.round === round) ??
null;
const bet =
ofKind(records, "decision").find(
(d) => d.content.decision === "bet" && d.content.round === round,
) ?? null;
jsonFile(join(roundDir, "settlement.json"), { settlement, bet });
// 收尾全文(第 5 样)。
const wrapUps = finals.filter(
(s) =>
s.content.kind === "wrapUp" && roundOf(s.content.stageId) === round,
);
jsonFile(join(roundDir, "wrapups.json"), wrapUps);
writeFileSync(
join(roundDir, "wrapups.txt"),
wrapUps
.map(
(s) =>
`${"=".repeat(20)} ${s.content.author}(记录 ${s.seq})\n${(s.content.content as { body: string }).body}`,
)
.join("\n\n"),
);
// 消融格(第 3 样):冻结协议、位置表、完整代码与格代码及 diff、alsoOff、concerns、每次核验逐项结论与退回、重做记录。
const cells = [];
if (chosenCells !== null) {
const adopt = board.decision("adopt", round)!;
const candidate = adopt.output.adopted!;
const full = board.code(candidate);
const baseline = board.code(board.baselineOf(round));
for (const key of chosenCells) {
const cell = cellId(candidate, key);
const cellDir = join(roundDir, "cells", cell);
mkdirSync(cellDir, { recursive: true });
jsonFile(join(cellDir, "protocol.json"), {
record: ofKind(records, "protocol")
.filter((p) => p.content.candidate === candidate)
.at(-1),
resolvedMetrics: protocolMetrics(
board,
board.protocol(candidate),
),
factorLocations: full.locations,
baselineVersion: baseline.version,
fullVersion: full.version,
});
checkout(full.version, join(cellDir, "full"));
checkout(baseline.version, join(cellDir, "baseline"));
// 每次实现和代码版本对号(同一事务里先写提交、紧接着写代码;交"跑不起来"时没有代码记录)。
const impls = finals.filter(
(s) =>
s.content.kind === "ablationImplementation" &&
s.content.target === cell,
);
const codes = ofKind(records, "code").filter(
(c) => c.content.object === cell,
);
const attempts = impls.map((s) => ({
submission: s,
code: codes.find((c) => c.seq === s.seq + 1) ?? null,
}));
jsonFile(join(cellDir, "implementations.json"), attempts);
const verifications = finals
.filter(
(s) =>
s.content.kind === "ablationVerification" &&
s.content.target === cell,
)
.map((s) => {
const impl =
impls.filter((x) => x.seq < s.seq).at(-1) ?? null;
return {
submission: s,
judgedImplSeq: impl?.seq ?? null,
outcome:
impl === null
? null
: ablationOutcome(
s.content.content as Parameters<
typeof ablationOutcome
>[0],
key,
(
impl.content.content as {
alsoOff: string[];
}
).alsoOff,
),
};
});
jsonFile(join(cellDir, "verifications.json"), verifications);
jsonFile(
join(cellDir, "rejections.json"),
ofKind(records, "rejection").filter(
(r) => r.content.target === cell,
),
);
const lastCode = codes.at(-1) ?? null;
if (lastCode !== null) {
checkout(lastCode.content.version, join(cellDir, "cell"));
jsonFile(
join(cellDir, "diff.json"),
compareVersions(
repo,
full.version,
lastCode.content.version,
),
);
}
for (const attempt of attempts) {
if (attempt.code === null) continue;
const path = join(cellDir, `code-${attempt.code.seq}`);
checkout(attempt.code.content.version, path);
jsonFile(
join(cellDir, `diff-${attempt.code.seq}.json`),
compareVersions(
repo,
full.version,
attempt.code.content.version,
),
);
}
jsonFile(
join(cellDir, "calls.json"),
ofKind(records, "callStarted")
.filter((c) => {
const material = c.content.materials as TaskMaterial;
return (
material.object === cell &&
[
"ablationImplementation",
"ablationVerification",
].includes(material.kind)
);
})
.map((c) => ({
record: c,
sessions: join(
study,
"calls",
c.content.call,
"sessions",
),
})),
);
cells.push({
cell,
factor: splitCell(cell).key,
dir: cellDir,
ran: lastCode !== null,
});
}
}
writeFileSync(
join(roundDir, "review.txt"),
`任务书17第3.2节,只读本目录,中文直接回复,不写文件。\n` +
`第1样(解释):读 explanations.txt、cells-decision.json、settlement.json。看不到消融结果地回答:机制是否具体、写了什么情况下不成立没有、三个数和正文是否一致、范围宽窄有没有依据、同轮几人的解释有没有真正不同、"没有具体机制判断"用得诚不诚实;再对 settlement.json 看每条解释在每格被确认还是否定。\n` +
`第2样(选格):读 cells-decision.json,列出各格合并范围、各组合信息价值/成本/净值、选中和没选的原因。\n` +
`第3样(消融实现与核验,每格一个全新上下文):读 cells/<格>/ 下的 protocol.json、full/、cell/、diff.json、implementations.json、verifications.json、rejections.json。判断:是不是只关了指定的因子;保障原样保留没有、关掉的因子的指标写 null 没有;答案路径标签和关掉以后的分支对不对得上;核验员每次逐项结论判得对不对;重做的只改了被指出的地方没有。\n` +
`第5样(收尾与接力):读 wrapups.txt、settlement.json,再对着 round-${round + 1}/proposals.txt(没有就是最后一轮)看下一轮提案用上了哪些发现。\n`,
);
index.push({
round,
dir: roundDir,
hasAblation: chosenCells !== null,
cells,
});
}
jsonFile(join(output, "index.json"), { study: dir, rounds: index });
console.log(`审阅材料:${output}`);