import { BATCH_RULES, CELL_RULES, type BatchRuleName, type CellRuleName } from "../rules/index.ts"; import type { ResearchConfig } from "./schema.ts"; // 步骤清单(docs/结构.md 第三部分,方法.md 第十一部分):把配置里的流程、开关、规则翻译成这份研究要走的步骤。 // flow、导出、恢复都只读这份清单,不再读配置里的开关。被关掉的步骤也列出来,标"按配置跳过"并写明原因, // flow 据此在账本里记一条"按配置跳过"。步骤里的先后、"若采纳的改法有 2 个以上因子"这类运行时条件归 flow。 /** 整份研究的步骤,按先后排。 */ export const STUDY_STEPS = [ "freezeConfig", // 1. 冻结配置 "baselineRun", // 2. 基线运行 "entryDraft", // 3. 第零轮:闭门写入口 "discussion1", // 3. 第零轮:第 1 次讨论 "discussion2", // 3. 第零轮:第 2 次讨论 "claim", // 3. 第零轮:认领 "assignEntries", // 3. 第零轮:分配(程序) "rounds", // 4. 一轮接一轮,直到余额用完;每一轮走 ROUND_STEPS "export", // 5. 导出 ] as const; export type StudyStepId = (typeof STUDY_STEPS)[number]; /** 每一轮的步骤,按先后排;注释里的编号是结构.md 第三部分"每一轮"的编号。 */ export const ROUND_STEPS = [ "openRound", // 1. 开轮:冻结本轮名单和提案上限 "carryOverPool", // 1. 开轮:处理留池候选 "proposal", // 2. 提案 "dedupe", // 3. 判重合并 "firstPrediction", // 4. 第一次预测(看原始提案) "critique", // 5. 挑刺 "response", // 6. 回应与统一 "freezeProtocol", // 6. 协议冻结:挑刺与回应关掉时也要冻结 "secondPrediction", // 7. 第二次预测(看冻结协议) "selectBatch", // 8. 选批 "implementation", // 9. 实现 "verification", // 9. 核验 "repair", // 9. 核验不通过:修复,再核验 "forwardRun", // 9. 正式运行 "publish", // 10. 整批公开,记分数账 "informationAccount", // 10. 记信息账(世界比特、惊讶、落点) "adopt", // 11. 采纳 "listCells", // 12. 列消融格 "explanation", // 12. 解释 "selectCells", // 12. 选格 "ablationImplementation", // 12. 消融实现 "ablationVerification", // 12. 消融核验 "ablationRun", // 12. 消融运行 "settle", // 12. 结算(含押的因子、消融中发现) "wrapUp", // 13. 收尾 ] as const; export type RoundStepId = (typeof ROUND_STEPS)[number]; /** 决策点用哪条规则:规则名和版本,从 rules 的登记表取。 */ export type RuleRef = { name: BatchRuleName | CellRuleName; version: number }; type Status = { status: "execute" } | { status: "skip"; reason: string }; /** 一步:执行,或按配置跳过(带原因);只有选批、选格两个决策点带规则,其余为 null。 */ export type Step = { id: S; rule: RuleRef | null } & Status; export interface StepList { /** 完整流程还是朴素循环;materials 据此决定给什么材料(朴素循环没有信念本和私人笔记)。 */ preset: ResearchConfig["flow"]["preset"]; study: Step[]; round: Step[]; } const EXECUTE: Status = { status: "execute" }; const skip = (reason: string): Status => ({ status: "skip", reason }); /** * 朴素循环照常做的步骤(方法.md 第十一部分):提案、判重、提案直接冻结、均匀随机选批、实现到正式运行、公开分数、采纳。 * 其余都不做:不预测、不挑刺不回应、不消融,没有第零轮、收尾,候选不留池;没有预测,也就没有信息账。 */ const NAIVE_STEPS: ReadonlySet = new Set([ "freezeConfig", "baselineRun", "rounds", "export", "openRound", "proposal", "dedupe", "freezeProtocol", "selectBatch", "implementation", "verification", "repair", "forwardRun", "publish", "adopt", ] as const); /** 把配置翻译成步骤清单。纯函数:同一份配置永远得到同一份清单。 */ export function stepList(config: ResearchConfig): StepList { const flow = config.flow; if (flow.preset === "naive") { const naive = skip("朴素循环不做这一步(方法.md 第十一部分)"); return assemble("naive", (id) => (NAIVE_STEPS.has(id) ? EXECUTE : naive), batchRule("uniformRandom"), null); } // 完整流程:只有开关关掉的步骤跳过。 const skipped: Partial> = {}; const off = (ids: Array, reason: string) => { for (const id of ids) skipped[id] = skip(reason); }; const zero = flow.roundZero; if (!zero.enabled) { off(["entryDraft", "discussion1", "discussion2", "claim", "assignEntries"], "配置关掉了第零轮"); } else if (config.models.researchers.length === 1) { // N = 1 时不讨论,直接把第一志愿分给他(方法.md 4.2"分配规则")。 off(["discussion1", "discussion2"], "只有一位研究者,第零轮不讨论(方法.md 4.2)"); } else if (zero.discussions === 1) { off(["discussion2"], "第零轮讨论次数配置为 1"); } if (!flow.firstPrediction) off(["firstPrediction"], "配置关掉了第一次预测"); // 挑刺与回应关掉时,协议照样冻结(freezeProtocol 不跟着关)。 if (!flow.critique) off(["critique", "response"], "配置关掉了挑刺与回应"); // 消融关掉时解释照样写文字,只是不给每格的三个数(方法.md 第十一部分"步骤开关"),所以 explanation 不跟着关。 if (!flow.ablation.enabled) { off( ["listCells", "selectCells", "ablationImplementation", "ablationVerification", "ablationRun", "settle"], "配置关掉了消融", ); } const cells = flow.ablation.enabled ? cellRule(flow.ablation.cellRule) : null; return assemble("full", (id) => skipped[id] ?? EXECUTE, batchRule(flow.batchRule), cells); } function batchRule(name: BatchRuleName): RuleRef { return { name, version: BATCH_RULES[name].version }; } function cellRule(name: CellRuleName): RuleRef { return { name, version: CELL_RULES[name].version }; } function assemble( preset: StepList["preset"], status: (id: StudyStepId | RoundStepId) => Status, batch: RuleRef, cells: RuleRef | null, ): StepList { const ruleOf = (id: RoundStepId) => (id === "selectBatch" ? batch : id === "selectCells" ? cells : null); return { preset, study: STUDY_STEPS.map((id) => ({ id, rule: null, ...status(id) })), round: ROUND_STEPS.map((id) => ({ id, rule: ruleOf(id), ...status(id) })), }; }