feat: 配置 config 模块(任务书 04)

- src/config:research.json 的字段定义(TypeBox,全部必填、不认识的字段报错,每个字段带中文说明和类别)、一次列出全部错误的校验、步骤清单纯函数。
- 流程写成完整流程 / 朴素循环两选一,第零轮、消融写成开 / 关两选一,冲突组合写不出来,不另写检查。
- 规则名取自 rules 登记表,config 改为依赖 rules(结构.md 第二部分、.dependency-cruiser.cjs)。
- ledger 顺手两处(结构.md 第四部分):消融结算按轮次唯一、去掉消融编号;私人笔记去掉版本号;表结构版本号升到 2。
- 结构.md 第八部分补 config 对外内容、更新 ledger 一节;WORK_LOG 更新。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 08:08:18 -04:00
parent 4dc7ac9d62
commit d2bfb971ab
13 changed files with 1045 additions and 20 deletions
+136
View File
@@ -0,0 +1,136 @@
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<S extends string> = { id: S; rule: RuleRef | null } & Status;
export interface StepList {
/** 完整流程还是朴素循环;materials 据此决定给什么材料(朴素循环没有信念本和私人笔记)。 */
preset: ResearchConfig["flow"]["preset"];
study: Step<StudyStepId>[];
round: Step<RoundStepId>[];
}
const EXECUTE: Status = { status: "execute" };
const skip = (reason: string): Status => ({ status: "skip", reason });
/**
* 朴素循环照常做的步骤(方法.md 第十一部分):提案、判重、提案直接冻结、均匀随机选批、实现到正式运行、公开分数、采纳。
* 其余都不做:不预测、不挑刺不回应、不消融,没有第零轮、收尾,候选不留池;没有预测,也就没有信息账。
*/
const NAIVE_STEPS: ReadonlySet<StudyStepId | RoundStepId> = 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<Record<StudyStepId | RoundStepId, Status>> = {};
const off = (ids: Array<StudyStepId | RoundStepId>, 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) })),
};
}