feat: flow 第 1 部分(骨架、研究开始、第零轮)与第零轮检验
按任务书 12 实现 src/flow:从账本现算下一步的纯函数、主循环、恢复入口、派模型任务和跑正式运行两个共用动作、冻结配置、基线、第零轮三个研究者阶段和入口分配,走到第 1 轮时停下。materials 导出 finalEntries、加 researcherId 和 StudyRecord。 审阅采纳两条:研究编号本机已用过时冻结前报错;docker run 没启动时写完结果就停下,不在本进程里无限起新编号。 eval/ 新增第零轮检验:起研究命令行、回放假模型、检验用 Jev 客户端、六问质量评估、16-real-1 回放、杀进程与两份同时跑。tsc 与 depcruise 也检查 eval/,新增 eval 只经 index.ts 用正式代码的规则。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { rmSync } from "node:fs";
|
||||
import type { Content } from "../ledger/index.ts";
|
||||
import { formalRun as runDefinition, judge, prepareJudging, prepareRun } from "../tasks/index.ts";
|
||||
import type { HypothesisMetric, JudgedResult, ProcessResult } from "../vocab/index.ts";
|
||||
import { checkoutVersion } from "../workspace/index.ts";
|
||||
import { containerName, freshDir, judgeContainerName, runPaths, workspaceRepo } from "./paths.ts";
|
||||
import type { Study } from "./study.ts";
|
||||
|
||||
// 共用动作二:跑一次正式运行(任务书 12 第 2.7 节)。先登记、再启动、再写结果,前后顺序由这个入口保证
|
||||
// (方法.md 第七、九部分):登记占名额,确认启动才扣次,确认没启动的写结果"没启动"、退还名额。
|
||||
// 判分不扣次数,不登记。恢复时接着等一次已经启动的运行,走同一个 finishRun。
|
||||
|
||||
export type RunPlan = Omit<Content<"runPlanned">, "definition">;
|
||||
|
||||
/**
|
||||
* 一次正式运行怎么结束:写完结果;被用户停下;或 docker run 没启动,附 Docker 的报错原文。
|
||||
* 没启动是外部错误,本进程不重试(CLAUDE.md"错误怎么处理",重试 0 次):调用方写停下事件,
|
||||
* 下次启动时"下一步做什么"照旧起新编号重跑。
|
||||
*/
|
||||
export type RunOutcome = "done" | "stopped" | { notStarted: string };
|
||||
|
||||
/** 跑一次正式运行:取出 version 的代码,在任务镜像里跑,判分,写结果。用户停下时不等容器,交回 "stopped"。 */
|
||||
export async function formalRun(s: Study, plan: RunPlan, version: string, metrics: HypothesisMetric[]): Promise<RunOutcome> {
|
||||
const definition = runDefinition(s.task, s.frozen.image, String(s.frozen.seed));
|
||||
s.ledger.append("runPlanned", { ...plan, definition });
|
||||
const paths = runPaths(s.dir, plan.run);
|
||||
freshDir(paths.base);
|
||||
checkoutVersion(workspaceRepo(s.dir), version, freshDir(paths.code));
|
||||
prepareRun(s.task, paths.code, freshDir(paths.run), metrics);
|
||||
const started = await s.runner.start(containerName(s.frozen.config.studyId, plan.run), definition, paths.run);
|
||||
if (!started.started) {
|
||||
s.ledger.append("runResult", { run: plan.run, process: { ending: "notStarted", reason: started.reason }, judged: null });
|
||||
return { notStarted: started.reason };
|
||||
}
|
||||
s.ledger.append("runStarted", { run: plan.run });
|
||||
return finishRun(s, plan.run, metrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等一次已经确认启动的运行结束,判分,写结果,删掉容器。日志写进这次运行自己的日志目录。
|
||||
* 收到"停下"信号就不等了,直接交回 "stopped";容器不管它,下次启动时恢复入口再来接着等。
|
||||
*/
|
||||
export async function finishRun(s: Study, run: string, metrics: HypothesisMetric[]): Promise<"done" | "stopped"> {
|
||||
const name = containerName(s.frozen.config.studyId, run);
|
||||
const paths = runPaths(s.dir, run);
|
||||
const waited = await untilStopped(s.stop, s.runner.wait(name, emptyDir(paths.logs)));
|
||||
if (waited === "stopped") return "stopped";
|
||||
const judged = await judgeRun(s, run, waited, metrics);
|
||||
s.ledger.append("runResult", { run, process: waited, judged });
|
||||
await s.runner.remove(name);
|
||||
return "done";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判分:tasks 的 prepareJudging,要跑校验器就让 runner 在镜像里跑,再交给 judge。
|
||||
* 判分目录每次重新建空的(恢复时会再判一次);判分容器还在就先删掉再用。
|
||||
*/
|
||||
async function judgeRun(s: Study, run: string, formal: ProcessResult, metrics: HypothesisMetric[]): Promise<JudgedResult> {
|
||||
const paths = runPaths(s.dir, run);
|
||||
const step = prepareJudging(s.task, paths.run, emptyDir(paths.judge), s.frozen.image, formal, metrics);
|
||||
if (step.status === "judged") return step.result;
|
||||
const name = judgeContainerName(s.frozen.config.studyId, run);
|
||||
if ((await s.runner.status(name)) !== "absent") await s.runner.remove(name);
|
||||
const started = await s.runner.start(name, step.definition, paths.judge);
|
||||
if (!started.started) throw new Error(`运行 ${run} 的校验器容器没有启动:${started.reason}`);
|
||||
const validator = await s.runner.wait(name, emptyDir(paths.judgeLogs));
|
||||
await s.runner.remove(name);
|
||||
return judge(s.task, paths.run, paths.judge, formal, validator, metrics);
|
||||
}
|
||||
|
||||
/** 清空重建一个目录:恢复时会再等一次、再判一次,上一次没写完的日志和判分目录不留。 */
|
||||
function emptyDir(dir: string): string {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
return freshDir(dir);
|
||||
}
|
||||
|
||||
/** 等一件事做完,或者收到"停下"信号先交回 "stopped"。被放下的那个 Promise 之后的结果没人用。 */
|
||||
async function untilStopped<T>(stop: AbortSignal, work: Promise<T>): Promise<T | "stopped"> {
|
||||
let onStop = (): void => undefined;
|
||||
const stopped = new Promise<"stopped">((resolve) => {
|
||||
onStop = () => resolve("stopped");
|
||||
if (stop.aborted) onStop();
|
||||
else stop.addEventListener("abort", onStop, { once: true });
|
||||
});
|
||||
try {
|
||||
return await Promise.race([work, stopped]);
|
||||
} finally {
|
||||
stop.removeEventListener("abort", onStop);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user