Files
iomgaa 0ae495f8e8 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 条通过。
2026-10-04 14:09:09 -04:00

181 lines
8.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import type { TaskMaterial } from "../../src/materials/index.ts";
import { cpuRatio, CPU_RATIO_FLAG, fullAccounts } from "../lib/full.ts";
import { frozenConfig, jsonFile } from "../lib/forward.ts";
import { ofKind, readLedger } from "../lib/ledger.ts";
import { lightCheckUsage } from "../lib/light-checks.ts";
import { readTimeline } from "../lib/timeline.ts";
// 任务书 17 第 3.2 节第 6、7 样的可计算部分:逐轮走向 + 程序账目 + CPU/墙钟比。只算事实、不作质量判断
// (解释质量、核验判得对不对、收尾与接力由父会话派全新上下文的子 Agent 读 eval/full/export.ts 的产物)。
// 这次三份研究同时跑,没有"启动区间互不重叠"这条(任务书 17 第 3.1 节);也不设总体 acceptance。
// node eval/full/stats.ts <全新绝对统计输出目录> <研究输出目录>...
const [output, ...dirs] = process.argv.slice(2);
if (!output || dirs.length === 0 || output !== resolve(output)) {
throw new Error(
"用法:node eval/full/stats.ts <全新绝对统计输出目录> <研究输出目录>...",
);
}
mkdirSync(output);
const fmt = (v: number | null) => (v === null ? "—" : v.toFixed(6));
const summaries = dirs.map((dir) => {
const studyDir = join(dir, "study");
const records = readLedger(studyDir);
const config = frozenConfig(records);
const accounts = fullAccounts(records);
// 轻量检查:每次实现/修复/消融实现调用用了几次、有没有超过配置上限(任务书 17 第 3.2 节第 7 样)。
const lightChecks = ofKind(records, "callStarted")
.filter((c) =>
["implementation", "repair", "ablationImplementation"].includes(
(c.content.materials as TaskMaterial).kind,
),
)
.map((c) => {
const sessions = join(studyDir, "calls", c.content.call, "sessions");
return {
call: c.content.call,
seq: c.seq,
...(existsSync(sessions)
? lightCheckUsage(sessions, config.lightChecks.maxCount)
: {
maxCount: config.lightChecks.maxCount,
pass: false,
note: "会话目录不存在",
}),
};
});
// 每次正式运行的 CPU 时间/墙钟时间之比;比值明显低于 1 的列出,并和 machine.jsonl 同时段负载放在一起(只列事实)。
const timeline = existsSync(join(dir, "timeline.jsonl"))
? readTimeline(join(dir, "timeline.jsonl"))
: [];
const atOf = (seq: number) => timeline.find((t) => t.seq === seq)?.at ?? null;
const machine = existsSync(join(dir, "machine.jsonl"))
? readFileSync(join(dir, "machine.jsonl"), "utf8")
.split("\n")
.filter(Boolean)
.map(
(line) =>
JSON.parse(line) as { at: string; label: string; load?: number[] },
)
: [];
const ratios = accounts.runs.map((r) => {
const result = r.results[0] ?? null;
const ratio = result === null ? null : cpuRatio(result.content);
const start = r.starts[0] === undefined ? null : atOf(r.starts[0].seq);
const end = result === null ? null : atOf(result.seq);
const samples =
start === null || end === null
? []
: machine
.filter(
(m) =>
m.label === "sample" &&
Date.parse(m.at) >= start &&
Date.parse(m.at) <= end,
)
.map((m) => ({ at: m.at, load: m.load }));
return {
run: r.plan.content.run,
purpose: r.plan.content.purpose,
round: r.plan.content.round,
object: r.plan.content.object,
ratio,
flagged: ratio !== null && ratio < CPU_RATIO_FLAG,
start: start === null ? null : new Date(start).toISOString(),
end: end === null ? null : new Date(end).toISOString(),
loadSamples: samples,
note: `比值低于 ${CPU_RATIO_FLAG} 列出并附同时段负载;只列事实,不作判断`,
};
});
// 有 kill-report.json 的研究,把三条检查的结果抄进来(任务书 17 第 3.1 节)。
const kill = existsSync(join(dir, "kill-report.json"))
? (JSON.parse(readFileSync(join(dir, "kill-report.json"), "utf8")) as {
evidence: unknown;
checks: Record<string, boolean>;
})
: null;
const pass =
Object.values(accounts.checks).every(Boolean) &&
lightChecks.every((c) => c.pass) &&
(kill === null || Object.values(kill.checks).every(Boolean));
return {
dir,
study: config.studyId,
accounts,
lightChecks,
ratios,
kill,
pass,
};
});
jsonFile(join(output, "stats.json"), summaries);
const text = summaries.map((s) => {
const a = s.accounts;
const lines = [
`研究 ${s.study}(${s.dir})`,
`确认启动:基线 ${a.confirmedStarts.baseline}、正向 ${a.confirmedStarts.forward}、消融 ${a.confirmedStarts.ablation},共 ${a.confirmedStarts.total};扣次 ${a.balance.charged},余额 ${a.balance.balance},占用 ${a.balance.reserved};已知最好 ${fmt(a.knownBest)}`,
`停下:${a.halt === null ? "(没有停下事件)" : a.halt.reason}`,
`账目核对:${Object.entries(a.checks)
.map(([k, v]) => `${k}=${v ? "过" : "不过"}`)
.join("、")}`,
`轻量检查:${s.lightChecks.every((c) => c.pass) ? "全部在上限内" : "有超限或会话缺失,见 stats.json"}`,
`CPU/墙钟比低于 ${CPU_RATIO_FLAG} 的运行:${
s.ratios
.filter((r) => r.flagged)
.map((r) => `${r.run}=${r.ratio!.toFixed(3)}`)
.join("、") || "没有"
}`,
s.kill === null
? "杀进程:这份没做"
: `杀进程:${Object.entries(s.kill.checks)
.map(([k, v]) => `${k}=${v ? "过" : "不过"}`)
.join("、")}`,
"",
];
for (const r of a.rounds) {
lines.push(
`第 ${r.round} 轮(基线 ${r.baseline})`,
...r.candidates.map(
(c) =>
` 候选 ${c.candidate}:实测 ${fmt(c.score)},合并 ${c.merged === null ? "—" : `${fmt(c.merged.q1)}/${fmt(c.merged.q5)}/${fmt(c.merged.q9)}`},落点 ${c.band ?? "—"},作者自评 ${c.authorEstimates.map((u) => `${u.author} ${fmt(u.range.q1)}/${fmt(u.range.q5)}/${fmt(u.range.q9)}`).join("、") || "—"}${c.notRunEvents.length > 0 ? ",不跑:" + c.notRunEvents[0]!.reason : ""}`,
),
` 采纳:${r.adopted ?? "没有"}${r.adoptedScore === null ? "" : `,分数 ${fmt(r.adoptedScore)},离已知最好 ${fmt(r.knownBestGap)}`}`,
);
if (r.hasAblation) {
lines.push(
` 消融:选格 ${r.cellsDecision!.output.chosen.join("、") || "(一格都没选)"};配合关系 ${r.label === null ? "—" : r.label.label};押的因子 ${r.bet === null ? "—" : `${r.bet.decision === "bet" ? r.bet.factor : `不押(${r.bet.decision})`},预测收益 ${fmt(r.bet.predictedGain)}`}`,
...r.cells.map(
(c) =>
` ${c.cell}:核验 ${c.verificationCount} 次、重做 ${c.redoCount} 次${c.cannotRun === null ? "" : ",跑不起来:" + c.cannotRun}${c.notRunEvents.length > 0 ? ",不跑:" + c.notRunEvents[0]!.reason : ""},跑没跑 ${c.ran ? "跑了" : "没跑"}(扣次 ${c.charged}),实测 ${fmt(c.measured)},撑几成 ${c.support === null ? "—" : c.support.toFixed(3)},合并 ${c.merged === null ? "—" : `${fmt(c.merged.q1)}/${fmt(c.merged.q5)}/${fmt(c.merged.q9)}`},结局 ${c.outcome ?? "—"}`,
),
...r
.cellsDecision!.output.cells.filter((c) => !c.tested)
.map(
(c) =>
` ${c.factor} 没测:${c.reason}(含它的最好组合净值 ${c.bestCombo === null ? "没有可选组合" : c.bestCombo.net.toExponential(3)})`,
),
);
} else lines.push(" 消融:这一轮没有");
lines.push(
` 解释关闭早于首格启动:${r.explanationBeforeCells === null ? "无消融" : r.explanationBeforeCells ? "是" : "否"};解封在收尾关闭之后且覆盖选中候选:${r.unseal.pass ? "是" : "否"}`,
);
lines.push("");
}
return lines.join("\n");
});
writeFileSync(join(output, "stats.txt"), `${text.join("\n")}\n`);
console.log(
`统计:${output};所给 ${summaries.length} 份研究的逐研究账目检查${summaries.every((s) => s.pass) ? "通过" : "未通过"}。不设总体 acceptance(三份同时跑,无互不重叠条款);研究质量判断仍待独立审阅。`,
);
if (!summaries.every((s) => s.pass)) process.exitCode = 1;