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:
@@ -0,0 +1,266 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { checkConfig, stepList, type ResearchConfig, type StepList } from "../src/config/index.ts";
|
||||
import { fullConfig, naiveConfig } from "./config-helpers.ts";
|
||||
|
||||
// 每份预期都对照 docs/结构.md 第三部分"步骤清单"和 docs/方法.md 第十一部分逐条手写,不从实现里推。
|
||||
// 记法:[步骤, 执行 / 跳过, 规则];规则写名字和版本,版本取 rules 登记表里写的 1。
|
||||
|
||||
type Row = [string, "execute" | "skip", { name: string; version: number } | null];
|
||||
const E = "execute" as const;
|
||||
const S = "skip" as const;
|
||||
|
||||
function rows(steps: StepList["study"] | StepList["round"]): Row[] {
|
||||
return steps.map((s) => [s.id, s.status, s.rule]);
|
||||
}
|
||||
|
||||
function listOf(change: (flow: Extract<ResearchConfig["flow"], { preset: "full" }>) => void): StepList {
|
||||
const config = fullConfig();
|
||||
change(config.flow as Extract<ResearchConfig["flow"], { preset: "full" }>);
|
||||
return stepList(checkConfig(config));
|
||||
}
|
||||
|
||||
/** 跳过的步骤都写了原因。 */
|
||||
function assertReasons(list: StepList): void {
|
||||
for (const s of [...list.study, ...list.round]) {
|
||||
if (s.status === "skip") assert.ok(s.reason.length > 0, `${s.id} 跳过却没写原因`);
|
||||
}
|
||||
}
|
||||
|
||||
test("步骤清单:完整流程全开", () => {
|
||||
const list = stepList(fullConfig());
|
||||
assert.equal(list.preset, "full");
|
||||
assert.deepEqual(rows(list.study), [
|
||||
["freezeConfig", E, null],
|
||||
["baselineRun", E, null],
|
||||
["entryDraft", E, null],
|
||||
["discussion1", E, null],
|
||||
["discussion2", E, null],
|
||||
["claim", E, null],
|
||||
["assignEntries", E, null],
|
||||
["rounds", E, null],
|
||||
["export", E, null],
|
||||
]);
|
||||
assert.deepEqual(rows(list.round), [
|
||||
["openRound", E, null],
|
||||
["carryOverPool", E, null],
|
||||
["proposal", E, null],
|
||||
["dedupe", E, null],
|
||||
["firstPrediction", E, null],
|
||||
["critique", E, null],
|
||||
["response", E, null],
|
||||
["freezeProtocol", E, null],
|
||||
["secondPrediction", E, null],
|
||||
["selectBatch", E, { name: "bestExpected", version: 1 }],
|
||||
["implementation", E, null],
|
||||
["verification", E, null],
|
||||
["repair", E, null],
|
||||
["forwardRun", E, null],
|
||||
["publish", E, null],
|
||||
["informationAccount", E, null],
|
||||
["adopt", E, null],
|
||||
["listCells", E, null],
|
||||
["explanation", E, null],
|
||||
["selectCells", E, { name: "valueMinusCost", version: 1 }],
|
||||
["ablationImplementation", E, null],
|
||||
["ablationVerification", E, null],
|
||||
["ablationRun", E, null],
|
||||
["settle", E, null],
|
||||
["wrapUp", E, null],
|
||||
]);
|
||||
});
|
||||
|
||||
test("步骤清单:只关第零轮", () => {
|
||||
const list = listOf((f) => (f.roundZero = { enabled: false }));
|
||||
assertReasons(list);
|
||||
assert.equal(list.preset, "full");
|
||||
assert.deepEqual(rows(list.study), [
|
||||
["freezeConfig", E, null],
|
||||
["baselineRun", E, null],
|
||||
["entryDraft", S, null],
|
||||
["discussion1", S, null],
|
||||
["discussion2", S, null],
|
||||
["claim", S, null],
|
||||
["assignEntries", S, null],
|
||||
["rounds", E, null],
|
||||
["export", E, null],
|
||||
]);
|
||||
assert.deepEqual(rows(list.round), rows(stepList(fullConfig()).round));
|
||||
});
|
||||
|
||||
test("步骤清单:只关第一次预测", () => {
|
||||
const list = listOf((f) => (f.firstPrediction = false));
|
||||
assertReasons(list);
|
||||
assert.deepEqual(rows(list.study), rows(stepList(fullConfig()).study));
|
||||
assert.deepEqual(rows(list.round), [
|
||||
["openRound", E, null],
|
||||
["carryOverPool", E, null],
|
||||
["proposal", E, null],
|
||||
["dedupe", E, null],
|
||||
["firstPrediction", S, null],
|
||||
["critique", E, null],
|
||||
["response", E, null],
|
||||
["freezeProtocol", E, null],
|
||||
["secondPrediction", E, null],
|
||||
["selectBatch", E, { name: "bestExpected", version: 1 }],
|
||||
["implementation", E, null],
|
||||
["verification", E, null],
|
||||
["repair", E, null],
|
||||
["forwardRun", E, null],
|
||||
["publish", E, null],
|
||||
["informationAccount", E, null],
|
||||
["adopt", E, null],
|
||||
["listCells", E, null],
|
||||
["explanation", E, null],
|
||||
["selectCells", E, { name: "valueMinusCost", version: 1 }],
|
||||
["ablationImplementation", E, null],
|
||||
["ablationVerification", E, null],
|
||||
["ablationRun", E, null],
|
||||
["settle", E, null],
|
||||
["wrapUp", E, null],
|
||||
]);
|
||||
});
|
||||
|
||||
test("步骤清单:只关挑刺与回应,协议照样冻结", () => {
|
||||
const list = listOf((f) => (f.critique = false));
|
||||
assertReasons(list);
|
||||
assert.deepEqual(rows(list.study), rows(stepList(fullConfig()).study));
|
||||
assert.deepEqual(rows(list.round), [
|
||||
["openRound", E, null],
|
||||
["carryOverPool", E, null],
|
||||
["proposal", E, null],
|
||||
["dedupe", E, null],
|
||||
["firstPrediction", E, null],
|
||||
["critique", S, null],
|
||||
["response", S, null],
|
||||
["freezeProtocol", E, null],
|
||||
["secondPrediction", E, null],
|
||||
["selectBatch", E, { name: "bestExpected", version: 1 }],
|
||||
["implementation", E, null],
|
||||
["verification", E, null],
|
||||
["repair", E, null],
|
||||
["forwardRun", E, null],
|
||||
["publish", E, null],
|
||||
["informationAccount", E, null],
|
||||
["adopt", E, null],
|
||||
["listCells", E, null],
|
||||
["explanation", E, null],
|
||||
["selectCells", E, { name: "valueMinusCost", version: 1 }],
|
||||
["ablationImplementation", E, null],
|
||||
["ablationVerification", E, null],
|
||||
["ablationRun", E, null],
|
||||
["settle", E, null],
|
||||
["wrapUp", E, null],
|
||||
]);
|
||||
});
|
||||
|
||||
test("步骤清单:只关消融,解释照样写,选格没有规则", () => {
|
||||
const list = listOf((f) => (f.ablation = { enabled: false }));
|
||||
assertReasons(list);
|
||||
assert.deepEqual(rows(list.study), rows(stepList(fullConfig()).study));
|
||||
assert.deepEqual(rows(list.round), [
|
||||
["openRound", E, null],
|
||||
["carryOverPool", E, null],
|
||||
["proposal", E, null],
|
||||
["dedupe", E, null],
|
||||
["firstPrediction", E, null],
|
||||
["critique", E, null],
|
||||
["response", E, null],
|
||||
["freezeProtocol", E, null],
|
||||
["secondPrediction", E, null],
|
||||
["selectBatch", E, { name: "bestExpected", version: 1 }],
|
||||
["implementation", E, null],
|
||||
["verification", E, null],
|
||||
["repair", E, null],
|
||||
["forwardRun", E, null],
|
||||
["publish", E, null],
|
||||
["informationAccount", E, null],
|
||||
["adopt", E, null],
|
||||
["listCells", S, null],
|
||||
["explanation", E, null],
|
||||
["selectCells", S, null],
|
||||
["ablationImplementation", S, null],
|
||||
["ablationVerification", S, null],
|
||||
["ablationRun", S, null],
|
||||
["settle", S, null],
|
||||
["wrapUp", E, null],
|
||||
]);
|
||||
});
|
||||
|
||||
test("步骤清单:朴素循环", () => {
|
||||
const list = stepList(naiveConfig());
|
||||
assertReasons(list);
|
||||
assert.equal(list.preset, "naive");
|
||||
assert.deepEqual(rows(list.study), [
|
||||
["freezeConfig", E, null],
|
||||
["baselineRun", E, null],
|
||||
["entryDraft", S, null],
|
||||
["discussion1", S, null],
|
||||
["discussion2", S, null],
|
||||
["claim", S, null],
|
||||
["assignEntries", S, null],
|
||||
["rounds", E, null],
|
||||
["export", E, null],
|
||||
]);
|
||||
assert.deepEqual(rows(list.round), [
|
||||
["openRound", E, null],
|
||||
["carryOverPool", S, null], // 候选不留池
|
||||
["proposal", E, null],
|
||||
["dedupe", E, null], // 判重照常
|
||||
["firstPrediction", S, null], // 不预测
|
||||
["critique", S, null], // 不挑刺
|
||||
["response", S, null], // 不回应
|
||||
["freezeProtocol", E, null], // 提案直接冻结
|
||||
["secondPrediction", S, null], // 不预测
|
||||
["selectBatch", E, { name: "uniformRandom", version: 1 }], // 均匀随机选批
|
||||
["implementation", E, null],
|
||||
["verification", E, null],
|
||||
["repair", E, null],
|
||||
["forwardRun", E, null],
|
||||
["publish", E, null],
|
||||
["informationAccount", S, null], // 没有预测,记不了信息账
|
||||
["adopt", E, null], // 采纳规则和完整流程相同
|
||||
["listCells", S, null], // 不消融
|
||||
["explanation", S, null],
|
||||
["selectCells", S, null],
|
||||
["ablationImplementation", S, null],
|
||||
["ablationVerification", S, null],
|
||||
["ablationRun", S, null],
|
||||
["settle", S, null],
|
||||
["wrapUp", S, null], // 没有收尾
|
||||
]);
|
||||
});
|
||||
|
||||
test("步骤清单:第零轮讨论 1 次时跳过第 2 次;只有一位研究者时不讨论", () => {
|
||||
const once = listOf((f) => (f.roundZero = { enabled: true, discussions: 1 }));
|
||||
assertReasons(once);
|
||||
assert.deepEqual(rows(once.study).slice(2, 7), [
|
||||
["entryDraft", E, null],
|
||||
["discussion1", E, null],
|
||||
["discussion2", S, null],
|
||||
["claim", E, null],
|
||||
["assignEntries", E, null],
|
||||
]);
|
||||
|
||||
const config = fullConfig();
|
||||
config.models.researchers = [config.models.researchers[0]!];
|
||||
const single = stepList(checkConfig(config));
|
||||
assertReasons(single);
|
||||
assert.deepEqual(rows(single.study).slice(2, 7), [
|
||||
["entryDraft", E, null],
|
||||
["discussion1", S, null],
|
||||
["discussion2", S, null],
|
||||
["claim", E, null],
|
||||
["assignEntries", E, null],
|
||||
]);
|
||||
});
|
||||
|
||||
test("步骤清单:换规则后清单里的规则跟着换", () => {
|
||||
const list = listOf((f) => {
|
||||
f.batchRule = "uniformRandom";
|
||||
f.ablation = { enabled: true, cellRule: "randomSameCount" };
|
||||
});
|
||||
const rule = (id: string) => list.round.find((s) => s.id === id)!.rule;
|
||||
assert.deepEqual(rule("selectBatch"), { name: "uniformRandom", version: 1 });
|
||||
assert.deepEqual(rule("selectCells"), { name: "randomSameCount", version: 1 });
|
||||
});
|
||||
Reference in New Issue
Block a user