import type { KindOf } from "../agents/index.ts"; import type { RoundStepId, StudyStepId } from "../config/index.ts"; import type { Content, LedgerRecord } from "../ledger/index.ts"; import { Board, ORIGINAL_BASELINE, researcherId } from "../materials/index.ts"; import { compareIds } from "../rules/index.ts"; import type { Stage } from "../vocab/index.ts"; import { readFrozen, type Frozen } from "./frozen.ts"; // "下一步做什么"(docs/结构.md 第二部分"几条边界"):步骤的先后只写在这里。 // 纯函数:输入账本记录(冻结的步骤清单就在其中的研究配置记录里),输出下一步要做的一件事; // 不读文件、不调模型,同样的记录永远得到同样的下一步。账本不存"进行到第几步",每次从记录现算。 // // 每一步做完以哪条记录为准,见 docs/结构.md 第八部分 flow 一节的表;下面每个步骤的分支各注一行。 // 没写成那条记录的步骤,下次会被重做或报错,不靠"这条记录公开了没有"这类隐含约定去猜。 /** 第零轮的研究者阶段:步骤 → 阶段名(vocab STAGES)和这个阶段交的提交种类。 */ const ROUND_ZERO_STAGES = { entryDraft: { stage: "entryDraft", kind: "entry" }, discussion1: { stage: "discussion", kind: "discussion" }, discussion2: { stage: "discussion", kind: "discussion" }, claim: { stage: "claim", kind: "claim" }, } as const satisfies Partial>; type RoundZeroStep = keyof typeof ROUND_ZERO_STAGES; /** 第 r 轮的研究者阶段:步骤名就是阶段名(vocab STAGES)。 */ type RoundStageStep = Extract; /** 阶段编号由 flow 起:r<轮>-<步骤>,一个步骤一个阶段。第 r 轮按配置跳过的步骤也用这个编号记"按配置跳过"。 */ export const stageIdOf = (round: number, step: string): string => `r${round}-${step}`; /** 基线运行编号:没启动就起下一个编号重跑(docs/结构.md 第四部分)。 */ export const baselineRunId = (n: number): string => `base-${n}`; /** flow 写的事件种类(账本记录 event)。 */ export const EVENTS = { /** 按配置跳过的步骤,objects 是步骤编号(第 r 轮的步骤写 r<轮>-<步骤>),reason 是 config 给的原因。 */ skipped: "skippedByConfig", /** 研究停下,reason 是停下的原因,objects 是相关的调用或运行。 */ halted: "halted", /** 恢复时问不到 Docker,不知道一次运行有没有启动(方法.md 第七部分"状态未知"),objects 是运行编号。 */ runStatusUnknown: "runStatusUnknown", } as const; /** 一个研究者任务:一个人对一个对象(候选、原提案;提案和第零轮的任务没有对象)交一种提交。 */ export type TaskRef = { actor: string; kind: KindOf<"researcher">; object: string | null }; export type Next = | { do: "freezeConfig" } | { do: "saveBaseline" } | { do: "baselineRun"; run: string } | { do: "skip"; step: string; reason: string } | { do: "openStage"; stageId: string; stage: Stage; round: number; roster: string[] } | { do: "closeStage"; stageId: string } | { do: "dispatch"; stageId: string; tasks: TaskRef[] } | { do: "assignEntries" } | { do: "dedupe"; round: number } | { do: "freezeProtocols"; round: number } | { do: "selectBatch"; round: number } | { do: "halt"; reason: string; objects: string[] }; export function nextStep(records: readonly LedgerRecord[]): Next { // 冻结配置 → 研究配置记录。 const frozen = readFrozen(records); if (frozen === null) return { do: "freezeConfig" }; const view = new View(records); for (const step of frozen.steps.study) { const next = step.status === "skip" ? skipNext(step.id, step.reason, view) : step.id === "rounds" ? roundsNext(view, frozen) : studyStepNext(step.id, view, frozen); if (next !== null) return next; } throw new Error("步骤清单走完了却没有停下:rounds 一步应当停下"); } /** 按配置跳过的步骤 → 一条"按配置跳过"事件,写明步骤和原因。 */ function skipNext(step: string, reason: string, view: View): Next | null { return view.events(EVENTS.skipped).some((e) => e.objects[0] === step) ? null : { do: "skip", step, reason }; } /** 整份研究的一个要执行的步骤还要做什么;做完了交回 null。 */ function studyStepNext(step: Exclude, view: View, frozen: Frozen): Next | null { switch (step) { case "freezeConfig": return null; case "baselineRun": return baselineNext(view); case "entryDraft": case "discussion1": case "discussion2": case "claim": { const { stage, kind } = ROUND_ZERO_STAGES[step]; return stageNext(stageIdOf(0, step), stage, 0, (roster) => roster.map((actor) => ({ actor, kind, object: null })), view, frozen); } case "assignEntries": // 分配 → 第 0 轮的入口分配决定(decision,种类 entries)。 return view.board.decision("entries", 0) === undefined ? { do: "assignEntries" } : null; case "export": throw new Error("走到了导出:rounds 一步应当先停下"); } } /** * 一轮接一轮(结构.md 第三部分"每一轮"):第一个没做完的轮次里第一个没做完的步骤。 * 每一步做完的记录都带轮次,做完的轮次整轮跳过;现在走到实现一步就停下,所以循环一定会返回。 */ function roundsNext(view: View, frozen: Frozen): Next { for (let round = 1; ; round++) { for (const step of frozen.steps.round) { const next = step.status === "skip" ? skipNext(stageIdOf(round, step.id), step.reason, view) : roundStepNext(step.id, round, view, frozen); if (next !== null) return next; } } } /** 第 round 轮的一个要执行的步骤还要做什么;做完了交回 null。 */ function roundStepNext(step: RoundStepId, round: number, view: View, frozen: Frozen): Next | null { switch (step) { case "openRound": case "carryOverPool": // 不写记录:本轮名单和提案上限就是冻结的配置(方法.md 4.3 ①),留池候选由 materials 从记录算出(Board.carried)。 return null; case "proposal": case "firstPrediction": case "critique": case "response": case "secondPrediction": return stageNext(stageIdOf(round, step), step, round, (roster) => roundTasks(step, round, roster, view.board), view, frozen); case "dedupe": // 判重合并 → 本轮的判重合并决定(decision,种类 dedup)。 return view.board.decision("dedup", round) === undefined ? { do: "dedupe", round } : null; case "freezeProtocol": { // 冻结协议 → 本轮每个新候选都有冻结协议记录(protocol)。全部候选在同一个事务里写; // 这一步排在回应阶段关闭之后,materials 把冻结协议的出现当作"已公开",早写会泄露。 if (frozen.steps.round.some((s) => s.id === "response" && s.status === "skip")) { return { do: "halt", reason: "配置关掉了挑刺与回应时怎么冻结协议还没写:submissions 的 protocolBody 只收回应和统一", objects: [] }; } const frozenIds = new Set(view.board.protocols().map((p) => p.candidate)); return freshOf(view.board, round).every((c) => frozenIds.has(c.candidate)) ? null : { do: "freezeProtocols", round }; } case "selectBatch": // 选批 → 本轮的选批决定(decision,种类 batch)。 return view.board.decision("batch", round) === undefined ? { do: "selectBatch", round } : null; case "implementation": return { do: "halt", reason: "实现还没写(任务书 15)", objects: [] }; default: throw new Error(`走到了第 ${round} 轮的 ${step}:implementation 一步应当先停下`); } } /** 第 round 轮判重合并出的新候选,以及各合并了哪几条原提案。 */ export function freshOf(board: Board, round: number): Array<{ candidate: string; proposals: string[] }> { return board.decision("dedup", round)!.output.fresh; } /** 第 round 轮的候选池:新候选加留池候选(方法.md 4.3 ①⑦)。第二次预测和选批都用这一份。 */ export function poolOf(board: Board, round: number): string[] { return [...freshOf(board, round).map((c) => c.candidate), ...board.carried(round)]; } /** * 第 r 轮一个研究者阶段的任务(任务书 14 第 2.3 节)。同一个人的几个任务按这里的先后跑。 * - 提案:每人一个任务,没有对象; * - 第一次预测:每人 × 本轮每个新候选(含自己的;留池候选不做,方法.md 4.3 ④); * - 挑刺:每人 × 本轮每条不是自己写的原提案(监督者定,照参考项目); * - 回应与统一:每个新候选一个任务,一条原提案的由作者交回应,几条合成的由其中编号最小的作者交统一(方法.md 4.3 ③⑥); * - 第二次预测:每人 × 候选池里每个候选。 */ function roundTasks(step: RoundStageStep, round: number, roster: readonly string[], board: Board): TaskRef[] { const each = (kind: TaskRef["kind"], objects: readonly string[]): TaskRef[] => roster.flatMap((actor) => objects.map((object) => ({ actor, kind, object }))); switch (step) { case "proposal": return roster.map((actor) => ({ actor, kind: "proposal", object: null })); case "firstPrediction": return each("firstPrediction", freshOf(board, round).map((c) => c.candidate)); case "critique": { const proposals = board.published("proposal").filter((p) => p.round === round).sort((a, b) => compareIds(a.item!, b.item!)); return roster.flatMap((actor) => proposals.filter((p) => p.author !== actor).map((p) => ({ actor, kind: "critique" as const, object: p.item! }))); } case "response": return freshOf(board, round).map((c) => ({ actor: board.authorsOf(c.candidate)[0]!, kind: c.proposals.length === 1 ? "response" : "unification", object: c.candidate, })); case "secondPrediction": return each("secondPrediction", poolOf(board, round)); } } /** * 基线 → 基线代码记录(对象 ORIGINAL_BASELINE),而且最近一次基线运行的结果不是"没启动"。 * 没启动的起新编号重跑;判为不算数的停下(方法.md 4.1)。 */ function baselineNext(view: View): Next | null { if (!view.of("code").some((c) => c.object === ORIGINAL_BASELINE)) return { do: "saveBaseline" }; const plans = view.of("runPlanned").filter((p) => p.purpose === "baseline"); const last = plans.at(-1); if (last === undefined) return { do: "baselineRun", run: baselineRunId(1) }; const result = view.of("runResult").find((r) => r.run === last.run); if (result === undefined) throw new Error(`基线运行 ${last.run} 登记了还没有结果:恢复入口应当已经核对过`); if (result.process.ending === "notStarted") return { do: "baselineRun", run: baselineRunId(plans.length + 1) }; if (result.judged?.verdict === "valid") return null; const why = result.judged === null ? result.process.ending : result.judged.verdictReason; return { do: "halt", reason: `基线运行 ${last.run} 判为不算数(${why}),不能当基线`, objects: [last.run] }; } /** * 研究者阶段 → 这个阶段的关闭记录(stageClose)。没开就开,名单是全部研究者(本轮名单就是冻结的配置); * 每个任务(一个人对一个对象)都有被收下的提交才关;没做完的任务按它最近一次调用怎么结束的来定派不派 * (任务书 12 第 2.3 节的表)。能派的先全派,派完还有任务停在"没交"才停下。 */ function stageNext( stageId: string, stage: Stage, round: number, tasksOf: (roster: readonly string[]) => TaskRef[], view: View, frozen: Frozen, ): Next | null { const open = view.board.stages.get(stageId); if (open === undefined) { return { do: "openStage", stageId, stage, round, roster: frozen.config.models.researchers.map((_, i) => researcherId(i)) }; } if (open.closed) return null; const todo = tasksOf(open.roster).filter((t) => !view.done(stageId, t)); if (todo.length === 0) return { do: "closeStage", stageId }; const tasks: TaskRef[] = []; const stuck: Array<{ call: string; reason: string }> = []; for (const t of todo) { const last = view.lastCall(stageId, t); // 没有调用、被用户停下、出错(含恢复时补写的"进程中断")都派;没交的只在开发入口开着时派(方法.md 第九部分"开发入口")。 if (last?.status === "notSubmitted" && !frozen.config.devEntry) stuck.push({ call: last.call, reason: last.reason }); else tasks.push(t); } if (tasks.length > 0) return { do: "dispatch", stageId, tasks }; return { do: "halt", reason: `交不出合格提交(方法.md 4.3 ②:按内部错误停下):${stuck.map((s) => `调用 ${s.call},${s.reason}`).join(";")}`, objects: stuck.map((s) => s.call), }; } /** 按种类取记录内容的小工具,加上 materials 读账本的那一份(Board)。 */ class View { readonly #records: readonly LedgerRecord[]; readonly board: Board; constructor(records: readonly LedgerRecord[]) { this.#records = records; this.board = new Board(records); } of(kind: K): Array> { return (this.#records.filter((r) => r.kind === kind) as Array>).map((r) => r.content); } events(event: string): Array> { return this.of("event").filter((e) => e.event === event); } /** 一个任务做完了:这个人在这个阶段对这个对象有一份被收下的提交(任务书 14 第 2.3 节)。 */ done(stageId: string, t: TaskRef): boolean { return this.of("submission").some((s) => s.stageId === stageId && s.author === t.actor && s.target === t.object); } /** * 一个任务最近一次模型调用的结束记录,没有调用为 null。"模型调用开始"里存的材料带着对象,按它认出是哪个任务。 * 开始了没结束的,恢复入口应当已经补写。 */ lastCall(stageId: string, t: TaskRef): Content<"callEnded"> | null { const started = this.of("callStarted") .filter((c) => c.stageId === stageId && c.actor === t.actor && (c.materials as { object: string | null }).object === t.object) .at(-1); if (started === undefined) return null; const ended = this.of("callEnded").find((e) => e.call === started.call); if (ended === undefined) throw new Error(`模型调用 ${started.call} 开始了还没有结束记录:恢复入口应当已经补写`); return ended; } }