Files
InfoRSI/tests/flow-next-step.test.ts
T
iomgaa 144b88480b flow: 消融到收尾、轮末解封、一轮接一轮与研究结束(任务书 17 第二块)
- ⑬–⑱:listCells 从采纳决定和冻结协议现算(不写记录);explanation 有消融才开
  阶段;selectCells 照 CellRuleInput 拼输入(余额当时现算、decisionSeed 版本 2),
  写种类 cells 的决定,没选中的格不另写事件;消融实现/核验按第几次编号
  (r<轮>-ablationImplementation-1、verification-1、-2……从账本数),重做从上一次
  这一格代码接着改,次数用完写 cellNotRunEvent 不跑不扣次;cannotRun 传 null 不核验
  不运行不扣次;alsoOff 用 sharedSettings 沿用同设置的第一格;消融运行一个接一个跑,
  用途 ablation,登记/扣次/恢复与正向同一入口,恢复时指标缺失策略覆盖为只记缺失
  (方法.md 第五部分);settle 拼 SettleInput 调 settleAblation,结算与 chooseBet 的
  bet 决定写在同一事务;wrapUp 阶段关闭后,本轮选中的每个候选在同一个事务里解封
  作者自评(取最后一版提案的封存)。
- 一轮接一轮:每轮开头用 runBalance 现算余额,余额为 0 不开新轮;走到 export 停下,
  原因"导出还没写"。去掉 roundsNext 里 step.id !== "listCells" 的临时写法。
- 消融中发现由 materials 的 Board.baselineOf 从结算输出读出,flow 不另写采纳记录
  (已核实 materials 已实现)。
- 测试 +14:下一步分支 8 条(含收尾关闭后才解封、余额 0 走到导出停、关掉消融时
  解释照方法停下)、消融步骤 6 条(选格输入逐字段手拼对账、结算四种结局、同设置
  沿用、解封取最后一版封存);npm run check 通过,npm test 472 条通过。
- 全新上下文审阅一轮(只看消融重做循环、登记扣次恢复、选格/结算/押的因子输入、
  解封时机、下一轮与结束条件):没有会污染账本的问题;采纳一条小修(删掉未用的
  导入),其余笔记照列不改。

flow 1302 → 1662 行(上限 3500),materials 未动(1930 行)。
2026-10-04 14:04:55 -04:00

686 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { test } from "node:test";
import { stepList, type ResearchConfig } from "../src/config/index.ts";
import type { AppendKind, Content, LedgerRecord } from "../src/ledger/index.ts";
import { Board, forwardNotRunEvent, type StudyRecord } from "../src/materials/index.ts";
import { runBalance } from "../src/rules/index.ts";
import { definition, exited } from "./ledger-helpers.ts";
import { judged } from "./materials-helpers.ts";
import { nextStep } from "../src/flow/next-step.ts";
import type { Json, Submission, SubmissionKind } from "../src/vocab/index.ts";
import { fullConfig } from "./config-helpers.ts";
// flow 的"下一步做什么"(任务书 12 第六部分):纯函数,手写账本记录,看它给出的下一步。
// 其余都由真实运行证明(任务书 12 第五部分),这里不写假模型、假 Docker。
const TASK_FACTS = JSON.parse(readFileSync(join(import.meta.dirname, "..", "tasks", "circle_packing", "task.json"), "utf8")) as Json;
const HASH = "0".repeat(64);
const measured = (value: number) => ({ status: "measured", value }) as const;
/** 调用结束记录去掉调用编号(四种结局各自去掉)。 */
type Ended = Content<"callEnded"> extends infer E ? (E extends unknown ? Omit<E, "call"> : never) : never;
/** 一本手写的账本:按写入先后编序号。内容照账本的形状写,nextStep 不再检查。 */
class Book {
readonly records: LedgerRecord[] = [];
add<K extends AppendKind>(kind: K, content: Content<K>): this {
this.records.push({ seq: this.records.length + 1, kind, hash: HASH, content } as LedgerRecord);
return this;
}
config(config: ResearchConfig): this {
const record: StudyRecord = {
config: config as unknown as Json,
steps: stepList(config) as unknown as Json,
prompts: { roles: {} as StudyRecord["prompts"]["roles"], tasks: {} as StudyRecord["prompts"]["tasks"] },
image: `sha256:${HASH}`,
task: TASK_FACTS,
seed: 1,
};
return this.add("config", record as unknown as Json);
}
baselineCode(): this {
return this.add("code", { object: "base", version: "v-base", locations: null });
}
/** 一次基线运行:没启动,或启动了、判分算数或不算数。 */
baselineRun(run: string, outcome: "notStarted" | "valid" | "invalid"): this {
const definition: Content<"runPlanned">["definition"] = {
command: ["python", "harness.py"],
workdir: "/work",
image: { digest: `sha256:${HASH}`, runtime: "runc" },
wallSeconds: 120,
cores: 1,
inheritEnv: false,
env: {},
};
this.add("runPlanned", { run, purpose: "baseline", round: 0, object: null, definition });
if (outcome === "notStarted") {
return this.add("runResult", { run, process: { ending: "notStarted", reason: "docker run 失败" }, judged: null });
}
this.add("runStarted", { run });
const process = { ending: "exited", exitCode: 0, cpuSeconds: measured(10), wallSeconds: measured(10), peakMemoryBytes: measured(1) } as const;
const judged = {
verdict: outcome,
verdictReason: outcome === "valid" ? "合法" : "不合法:圆重叠",
score: measured(0.9597642169962064),
metrics: {},
answerPath: { status: "unmeasured", reason: "基线不写" },
safeguardTriggers: { status: "unmeasured", reason: "基线不写" },
extra: null,
} as unknown as Content<"runResult">["judged"];
return this.add("runResult", { run, process, judged });
}
open(stageId: string, stage: Content<"stageOpen">["stage"], round: number, roster: string[]): this {
return this.add("stageOpen", { stageId, stage, round, roster });
}
/** 一份被收下的提交。nextStep 只看这个人在这个阶段有没有被收下的提交,种类一律写成入口。 */
entry(stageId: string, author: string): this {
const content = { entries: [{ name: "入口一", body: "从这里开始看" }, { name: "入口二", body: "从那里开始看" }] };
return this.add("submission", { stageId, author, target: null, item: null, version: 1, kind: "entry", content, jev: null });
}
/** 一份被收下的提交,内容 nextStep 不读。 */
sub(stageId: string, author: string, kind: SubmissionKind, target: string | null, item: string | null): this {
return this.add("submission", { stageId, author, target, item, version: 1, kind, content: {}, jev: null } as unknown as Content<"submission">);
}
/** 一次模型调用:开始和结束。"模型调用开始"里的材料只写 nextStep 认任务用的种类和对象。 */
call(stageId: string, actor: string, kind: SubmissionKind, object: string | null, n: number, ended: Ended): this {
const call = `${stageId}.${actor}.${kind}.${object ?? "none"}.${n}`;
this.add("callStarted", { call, actor, stageId, model: "iomgaa/glm-5.3", materials: { kind, object } });
return this.add("callEnded", { call, ...ended } as Content<"callEnded">);
}
}
const R = ["researcher-1", "researcher-2", "researcher-3"];
const entryTasks = (actors: string[]) => actors.map((actor) => ({ actor, kind: "entry", object: null }));
/** 冻结了配置、基线跑完算数的账本。 */
function afterBaseline(config: ResearchConfig = fullConfig()): Book {
return new Book().config(config).baselineCode().baselineRun("base-1", "valid");
}
test("空账本:冻结配置", () => {
assert.deepEqual(nextStep([]), { do: "freezeConfig" });
});
test("有配置、没有基线:先存基线代码,再用 base-1 跑基线", () => {
const book = new Book().config(fullConfig());
assert.deepEqual(nextStep(book.records), { do: "saveBaseline" });
book.baselineCode();
assert.deepEqual(nextStep(book.records), { do: "baselineRun", run: "base-1" });
});
test("基线最近一次是\"没启动\":起新编号再跑", () => {
const book = new Book().config(fullConfig()).baselineCode().baselineRun("base-1", "notStarted");
assert.deepEqual(nextStep(book.records), { do: "baselineRun", run: "base-2" });
book.baselineRun("base-2", "notStarted");
assert.deepEqual(nextStep(book.records), { do: "baselineRun", run: "base-3" });
});
test("基线判为不算数:停下,原因写明", () => {
const book = new Book().config(fullConfig()).baselineCode().baselineRun("base-1", "invalid");
const next = nextStep(book.records);
assert.equal(next.do, "halt");
assert.match((next as { reason: string }).reason, /base-1 判为不算数(不合法:圆重叠)/);
});
test("基线算数之后:开写入口阶段,名单是全部研究者", () => {
assert.deepEqual(nextStep(afterBaseline().records), { do: "openStage", stageId: "r0-entryDraft", stage: "entryDraft", round: 0, roster: R });
});
test("阶段开着、一人已交、两人没交:派那两人", () => {
const book = afterBaseline().open("r0-entryDraft", "entryDraft", 0, R).entry("r0-entryDraft", "researcher-1");
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r0-entryDraft", tasks: entryTasks(["researcher-2", "researcher-3"]) });
});
test("都交了:关阶段;关了以后开第 1 次讨论", () => {
const book = afterBaseline().open("r0-entryDraft", "entryDraft", 0, R);
for (const r of R) book.entry("r0-entryDraft", r);
assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r0-entryDraft" });
book.add("stageClose", { stageId: "r0-entryDraft" });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r0-discussion1", stage: "discussion", round: 0, roster: R });
});
test("有人最近一次是\"没交\":开发入口关着先派能派的、再停下;开着重新派", () => {
const stuck = (devEntry: boolean) =>
new Book()
.config({ ...fullConfig(), devEntry })
.baselineCode()
.baselineRun("base-1", "valid")
.open("r0-entryDraft", "entryDraft", 0, R)
.entry("r0-entryDraft", "researcher-1")
.call("r0-entryDraft", "researcher-2", "entry", null, 1, { wallSeconds: measured(30), status: "notSubmitted", reason: "模型结束了回复,没有一份提交被收下" });
// researcher-3 还没派过,先派他。
assert.deepEqual(nextStep(stuck(false).records), { do: "dispatch", stageId: "r0-entryDraft", tasks: entryTasks(["researcher-3"]) });
// researcher-3 交上以后只剩"没交"的 researcher-2:停下,附调用编号。
const closed = stuck(false).entry("r0-entryDraft", "researcher-3");
const next = nextStep(closed.records);
assert.equal(next.do, "halt");
assert.match((next as { reason: string }).reason, /^交不出合格提交/);
assert.deepEqual((next as { objects: string[] }).objects, ["r0-entryDraft.researcher-2.entry.none.1"]);
// 开发入口开着:重新派。
const dev = stuck(true).entry("r0-entryDraft", "researcher-3");
assert.deepEqual(nextStep(dev.records), { do: "dispatch", stageId: "r0-entryDraft", tasks: entryTasks(["researcher-2"]) });
});
test("有人最近一次是\"出错\"或\"进程中断\":重新派;只看最近一次调用", () => {
const book = afterBaseline()
.open("r0-entryDraft", "entryDraft", 0, R)
.entry("r0-entryDraft", "researcher-1")
.call("r0-entryDraft", "researcher-2", "entry", null, 1, { wallSeconds: measured(5), status: "notSubmitted", reason: "没交" })
.call("r0-entryDraft", "researcher-2", "entry", null, 2, { wallSeconds: measured(5), status: "failed", reason: "模型服务报错" })
.call("r0-entryDraft", "researcher-3", "entry", null, 1, { wallSeconds: { status: "unmeasured", reason: "进程中断" }, status: "failed", reason: "进程中断" });
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r0-entryDraft", tasks: entryTasks(["researcher-2", "researcher-3"]) });
});
test("只有一位研究者:两次讨论各写一条按配置跳过,接着开认领", () => {
const one = fullConfig();
one.models.researchers = [one.models.researchers[0]!];
const book = afterBaseline(one).open("r0-entryDraft", "entryDraft", 0, ["researcher-1"]).entry("r0-entryDraft", "researcher-1");
book.add("stageClose", { stageId: "r0-entryDraft" });
const reason = "只有一位研究者,第零轮不讨论(方法.md 4.2)";
assert.deepEqual(nextStep(book.records), { do: "skip", step: "discussion1", reason });
book.add("event", { event: "skippedByConfig", reason, objects: ["discussion1"] });
assert.deepEqual(nextStep(book.records), { do: "skip", step: "discussion2", reason });
book.add("event", { event: "skippedByConfig", reason, objects: ["discussion2"] });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r0-claim", stage: "claim", round: 0, roster: ["researcher-1"] });
});
/** 第零轮做完:四个阶段都交齐关闭,分配决定已写。 */
function afterRoundZero(config: ResearchConfig): Book {
const book = afterBaseline(config);
const stages = [
["r0-entryDraft", "entryDraft"],
["r0-discussion1", "discussion"],
["r0-discussion2", "discussion"],
["r0-claim", "claim"],
] as const;
for (const [stageId, stage] of stages) {
book.open(stageId, stage, 0, R);
for (const r of R) book.entry(stageId, r);
book.add("stageClose", { stageId });
}
return book;
}
/** 分配决定。 */
function assigned(book: Book): Book {
const output = R.map((researcher, i) => ({ researcher, entry: `e-${researcher}-1`, rank: i + 1 }));
return book.add("decision", { decision: "entries", round: 0, rule: "assignEntries", ruleVersion: 1, params: null, seed: null, input: null, output });
}
test("认领关了:分配;分配做完:开第 1 轮的提案阶段,名单是全部研究者", () => {
const book = afterRoundZero(fullConfig());
assert.deepEqual(nextStep(book.records), { do: "assignEntries" });
assigned(book);
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-proposal", stage: "proposal", round: 1, roster: R });
});
// ---------------------------------------------------------------------------
// 第 1 轮:开轮到选批(任务书 14 第 2.2、2.3 节)
// 三条原提案:researcher-1 的单独成候选 r1-c1;researcher-2、researcher-3 的判为同一个改法,合成 r1-c2。
// ---------------------------------------------------------------------------
const P1 = "r1-researcher-1-1";
const P2 = "r1-researcher-2-1";
const P3 = "r1-researcher-3-1";
const C = ["r1-c1", "r1-c2"];
/** 一个研究者阶段:开、每个任务交一份、关。tasks 是 [作者, 种类, 对象, 编号]。 */
function stage(book: Book, stageId: string, kind: Content<"stageOpen">["stage"], tasks: Array<[string, SubmissionKind, string | null, string | null]>): Book {
book.open(stageId, kind, 1, R);
for (const [author, k, target, item] of tasks) book.sub(stageId, author, k, target, item);
return book.add("stageClose", { stageId });
}
/** 每人 × 每个对象的任务。 */
const each = (kind: SubmissionKind, objects: string[]) => R.flatMap((actor) => objects.map((object) => ({ actor, kind, object })));
/** 第 1 轮写到 last 这一步做完为止。 */
function roundOne(last: "proposal" | "dedupe" | "firstPrediction" | "critique" | "response" | "freezeProtocol" | "secondPrediction" | "selectBatch"): Book {
const book = assigned(afterRoundZero(fullConfig()));
const order = ["proposal", "dedupe", "firstPrediction", "critique", "response", "freezeProtocol", "secondPrediction", "selectBatch"];
const steps: Record<string, () => void> = {
proposal: () => stage(book, "r1-proposal", "proposal", [["researcher-1", "proposal", null, P1], ["researcher-2", "proposal", null, P2], ["researcher-3", "proposal", null, P3]]),
dedupe: () => {
const output = { fresh: [{ candidate: "r1-c1", proposals: [P1] }, { candidate: "r1-c2", proposals: [P2, P3] }], carried: [] };
book.add("decision", { decision: "dedup", round: 1, rule: "jevSameChange", ruleVersion: 1, params: null, seed: null, input: null, output });
},
firstPrediction: () => stage(book, "r1-firstPrediction", "firstPrediction", R.flatMap((a) => C.map((c) => [a, "firstPrediction", c, null] as [string, SubmissionKind, string, null]))),
critique: () =>
stage(book, "r1-critique", "critique", [
["researcher-1", "critique", P2, `${P2}.researcher-1.1`],
["researcher-1", "critique", P3, `${P3}.researcher-1.1`],
["researcher-2", "critique", P1, `${P1}.researcher-2.1`],
["researcher-2", "critique", P3, `${P3}.researcher-2.1`],
["researcher-3", "critique", P1, `${P1}.researcher-3.1`],
["researcher-3", "critique", P2, `${P2}.researcher-3.1`],
]),
response: () => stage(book, "r1-response", "response", [["researcher-1", "response", "r1-c1", null], ["researcher-2", "unification", "r1-c2", null]]),
freezeProtocol: () => {
// r1-c1 一个因子(采纳它不做消融),r1-c2 两个因子(采纳它走 ⑬–⑰)。
book.add("protocol", { candidate: "r1-c1", protocol: { round: 1, baseline: "base", proposals: [], factors: [{}] } });
book.add("protocol", { candidate: "r1-c2", protocol: { round: 1, baseline: "base", proposals: [], factors: [{}, {}] } });
},
secondPrediction: () => stage(book, "r1-secondPrediction", "secondPrediction", R.flatMap((a) => C.map((c) => [a, "secondPrediction", c, null] as [string, SubmissionKind, string, null]))),
selectBatch: () => book.add("decision", { decision: "batch", round: 1, rule: "bestExpected", ruleVersion: 1, params: null, seed: "s", input: null, output: { chosen: ["r1-c1"] } }),
};
for (const step of order.slice(0, order.indexOf(last) + 1)) steps[step]!();
return book;
}
test("提案阶段一人交了两条、两人没交:只派没交的两人,提案没有对象", () => {
const book = assigned(afterRoundZero(fullConfig())).open("r1-proposal", "proposal", 1, R).sub("r1-proposal", "researcher-1", "proposal", null, P1);
book.sub("r1-proposal", "researcher-1", "proposal", null, "r1-researcher-1-2");
const tasks = ["researcher-2", "researcher-3"].map((actor) => ({ actor, kind: "proposal", object: null }));
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-proposal", tasks });
});
test("提案交齐关闭后:判重合并", () => {
assert.deepEqual(nextStep(roundOne("proposal").records), { do: "dedupe", round: 1 });
});
test("判重做完:开第一次预测;任务是每人 × 每个新候选(含自己的),同一个人的按候选编号排", () => {
const book = roundOne("dedupe");
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-firstPrediction", stage: "firstPrediction", round: 1, roster: R });
book.open("r1-firstPrediction", "firstPrediction", 1, R);
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-firstPrediction", tasks: each("firstPrediction", C) });
});
test("同一个人的任务按对象分开算:对 r1-c1 没交的停住,r1-c2 照派;能派的派完才停下", () => {
const notSubmitted = { wallSeconds: measured(30), status: "notSubmitted", reason: "模型结束了回复,没有一份提交被收下" } as const;
const book = roundOne("dedupe").open("r1-firstPrediction", "firstPrediction", 1, R);
for (const a of ["researcher-1", "researcher-3"]) for (const c of C) book.sub("r1-firstPrediction", a, "firstPrediction", c, null);
book.call("r1-firstPrediction", "researcher-2", "firstPrediction", "r1-c1", 1, notSubmitted);
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-firstPrediction", tasks: [{ actor: "researcher-2", kind: "firstPrediction", object: "r1-c2" }] });
book.sub("r1-firstPrediction", "researcher-2", "firstPrediction", "r1-c2", null);
const next = nextStep(book.records);
assert.equal(next.do, "halt");
assert.deepEqual((next as { objects: string[] }).objects, ["r1-firstPrediction.researcher-2.firstPrediction.r1-c1.1"]);
});
test("挑刺阶段:每人 × 每条不是自己写的原提案,没有挑自己的", () => {
const book = roundOne("firstPrediction").open("r1-critique", "critique", 1, R);
const expected = [
{ actor: "researcher-1", kind: "critique", object: P2 },
{ actor: "researcher-1", kind: "critique", object: P3 },
{ actor: "researcher-2", kind: "critique", object: P1 },
{ actor: "researcher-2", kind: "critique", object: P3 },
{ actor: "researcher-3", kind: "critique", object: P1 },
{ actor: "researcher-3", kind: "critique", object: P2 },
];
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-critique", tasks: expected });
});
test("回应阶段:单条原提案的候选派作者交回应,合并而成的派编号最小的作者交统一", () => {
const book = roundOne("critique").open("r1-response", "response", 1, R);
const expected = [
{ actor: "researcher-1", kind: "response", object: "r1-c1" },
{ actor: "researcher-2", kind: "unification", object: "r1-c2" },
];
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-response", tasks: expected });
});
test("回应阶段关闭、还没有冻结协议:写冻结协议;写完开第二次预测,任务是每人 × 池里每个候选", () => {
const book = roundOne("response");
assert.deepEqual(nextStep(book.records), { do: "freezeProtocols", round: 1 });
const frozen = roundOne("freezeProtocol");
assert.deepEqual(nextStep(frozen.records), { do: "openStage", stageId: "r1-secondPrediction", stage: "secondPrediction", round: 1, roster: R });
frozen.open("r1-secondPrediction", "secondPrediction", 1, R);
assert.deepEqual(nextStep(frozen.records), { do: "dispatch", stageId: "r1-secondPrediction", tasks: each("secondPrediction", C) });
});
test("第二次预测关闭:选批", () => {
assert.deepEqual(nextStep(roundOne("secondPrediction").records), { do: "selectBatch", round: 1 });
});
test("选批做完:开实现阶段,每个入选候选固定一位实验员", () => {
assert.deepEqual(nextStep(roundOne("selectBatch").records), { do: "openStage", stageId: "r1-implementation", stage: "implementation", round: 1, roster: ["experimenter-r1-c1"] });
});
test("关掉第一次预测:第 1 轮写一条按配置跳过(对象 r1-firstPrediction),接着开挑刺", () => {
const config = fullConfig();
if (config.flow.preset !== "full") throw new Error("测试配置应是完整流程");
config.flow.firstPrediction = false;
const book = assigned(afterRoundZero(config));
stage(book, "r1-proposal", "proposal", [["researcher-1", "proposal", null, P1], ["researcher-2", "proposal", null, P2], ["researcher-3", "proposal", null, P3]]);
const output = { fresh: [{ candidate: "r1-c1", proposals: [P1] }, { candidate: "r1-c2", proposals: [P2, P3] }], carried: [] };
book.add("decision", { decision: "dedup", round: 1, rule: "jevSameChange", ruleVersion: 1, params: null, seed: null, input: null, output });
const reason = "配置关掉了第一次预测";
assert.deepEqual(nextStep(book.records), { do: "skip", step: "r1-firstPrediction", reason });
book.add("event", { event: "skippedByConfig", reason, objects: ["r1-firstPrediction"] });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-critique", stage: "critique", round: 1, roster: R });
});
// ⑨–⑫:预期动作手写,不调模型、Jev 或 Docker。
const X = ["experimenter-r1-c1", "experimenter-r1-c2"];
const V = ["verifier-r1-c1", "verifier-r1-c2"];
function batchTwo(maxRepairs: number): Book {
const book = roundOne("secondPrediction");
const record = book.records.find((r) => r.kind === "config")!;
(record.content as unknown as StudyRecord).config = { ...fullConfig(), maxRepairs } as unknown as Json;
return book.add("decision", { decision: "batch", round: 1, rule: "bestExpected", ruleVersion: 1, params: null, seed: "s", input: null, output: { chosen: C } });
}
function implemented(book: Book): Book {
book.open("r1-implementation", "implementation", 1, X);
for (const [i, candidate] of C.entries()) book.sub("r1-implementation", X[i]!, "implementation", candidate, null);
return book.add("stageClose", { stageId: "r1-implementation" });
}
function verified(book: Book, n: number, candidate: string, passed: boolean): Book {
// 对应真实 15-tight-2 的 submissions created_seq=173(墙约束行和右端错位)、194(修复后通过)。
// 只转录驱动程序所需的逐项 found,不伪造 Jev 的判断或模型运行。
const content: Submission<"verification"> = {
clauses: [{ clause: "c1", found: passed, where: passed ? "initial.py 墙约束行和右端已对齐" : "initial.py:225-228 与 243-246:墙约束行和右端错位" }],
factors: [{ factor: "p1", found: passed, where: "LP 墙约束" }],
safeguards: [], unassigned: [], body: passed ? "通过" : "解的不是协议写的线性规划",
};
return book.add("submission", { stageId: `r1-verification-${n}`, author: `verifier-${candidate}`, kind: "verification", target: candidate, item: null, version: 1, content, jev: null });
}
function firstVerification(book: Book, a: boolean, b: boolean): Book {
implemented(book).open("r1-verification-1", "verification", 1, V);
verified(book, 1, C[0]!, a);
verified(book, 1, C[1]!, b);
return book.add("stageClose", { stageId: "r1-verification-1" });
}
function recordForward(book: Book, candidate: string, n: number, ending: "valid" | "notStarted" | "timeout"): Book {
const run = `r1-forward-${candidate}-${n}`;
book.add("runPlanned", { run, purpose: "forward", round: 1, object: candidate, definition });
if (ending === "notStarted") return book.add("runResult", { run, process: { ending, reason: "Docker 未启动" }, judged: null });
book.add("runStarted", { run });
const process = ending === "timeout" ? { ending: "timeout" as const, wallSeconds: measured(120), cpuSeconds: measured(100), peakMemoryBytes: measured(1024) } : exited;
return book.add("runResult", { run, process, judged: ending === "valid" ? judged(1.25, "main") : null });
}
const forward = (candidate: string, n: number) => ({ do: "forwardRun", round: 1, candidate, run: `r1-forward-${candidate}-${n}` });
test("实现的任务同时可派;已提交后中断不重派,等全体交齐关阶段后才核验", () => {
const book = batchTwo(1).open("r1-implementation", "implementation", 1, X);
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-implementation", tasks: C.map((object, i) => ({ actor: X[i], kind: "implementation", object })) });
book.sub("r1-implementation", X[0]!, "implementation", C[0]!, null);
book.call("r1-implementation", X[0]!, "implementation", C[0]!, 1, { wallSeconds: { status: "unmeasured", reason: "进程中断" }, status: "failed", reason: "进程中断" });
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-implementation", tasks: [{ actor: X[1], kind: "implementation", object: C[1] }] });
book.sub("r1-implementation", X[1]!, "implementation", C[1]!, null);
assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-implementation" });
book.add("stageClose", { stageId: "r1-implementation" });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-verification-1", stage: "verification", round: 1, roster: V });
});
test("15-tight-2 的一次修复路径:核验关后才派原实验员,修复关后只重验修过的候选", () => {
const book = implemented(batchTwo(1)).open("r1-verification-1", "verification", 1, V);
verified(book, 1, C[0]!, false);
verified(book, 1, C[1]!, true);
assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-verification-1" });
book.add("stageClose", { stageId: "r1-verification-1" });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-repair-1", stage: "implementation", round: 1, roster: [X[0]] });
book.open("r1-repair-1", "implementation", 1, [X[0]!]);
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-repair-1", tasks: [{ actor: X[0], kind: "repair", object: C[0] }] });
book.sub("r1-repair-1", X[0]!, "repair", C[0]!, null);
assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-repair-1" });
book.add("stageClose", { stageId: "r1-repair-1" });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-verification-2", stage: "verification", round: 1, roster: [V[0]] });
book.open("r1-verification-2", "verification", 1, [V[0]!]);
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-verification-2", tasks: [{ actor: V[0], kind: "verification", object: C[0] }] });
verified(book, 2, C[0]!, true);
assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-verification-2" });
book.add("stageClose", { stageId: "r1-verification-2" });
assert.deepEqual(nextStep(book.records), forward(C[0]!, 1));
});
test("修复次数从关闭的阶段数出:两次修复后仍失败,逐候选写不跑事件,不占预算", () => {
const book = firstVerification(batchTwo(2), false, false);
for (let n = 1; n <= 2; n++) {
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: `r1-repair-${n}`, stage: "implementation", round: 1, roster: X });
book.open(`r1-repair-${n}`, "implementation", 1, X);
for (const [i, candidate] of C.entries()) book.sub(`r1-repair-${n}`, X[i]!, "repair", candidate, null);
book.add("stageClose", { stageId: `r1-repair-${n}` });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: `r1-verification-${n + 1}`, stage: "verification", round: 1, roster: V });
book.open(`r1-verification-${n + 1}`, "verification", 1, V);
for (const candidate of C) verified(book, n + 1, candidate, false);
book.add("stageClose", { stageId: `r1-verification-${n + 1}` });
}
for (const candidate of C) {
const next = nextStep(book.records);
assert.equal(next.do, "forwardNotRun");
if (next.do !== "forwardNotRun") throw new Error("应写不跑事件");
assert.equal(next.candidate, candidate);
assert.match(next.reason, /第 3 次核验仍不通过,修复次数上限 2 已用完/);
assert.match(next.reason, /解的不是协议写的线性规划/);
book.add("event", forwardNotRunEvent(candidate, next.reason));
}
assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { charged: 1, reserved: 0, balance: 12 });
assert.deepEqual(nextStep(book.records), { do: "adopt", round: 1 });
});
test("maxRepairs=0:第一次核验失败就记不跑,另一候选仍可串行运行", () => {
const book = firstVerification(batchTwo(0), false, true);
const next = nextStep(book.records);
assert.equal(next.do, "forwardNotRun");
if (next.do !== "forwardNotRun") throw new Error("应写不跑事件");
book.add("event", forwardNotRunEvent(next.candidate, next.reason));
assert.deepEqual(nextStep(book.records), forward(C[1]!, 1));
});
test("正向串行:上次未启动起新编号;已登记未结束必须先恢复;超时也扣次且不重跑;采纳前不公开", () => {
const book = firstVerification(batchTwo(1), true, true);
assert.deepEqual(nextStep(book.records), forward(C[0]!, 1));
recordForward(book, C[0]!, 1, "notStarted");
assert.deepEqual(nextStep(book.records), forward(C[0]!, 2));
assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { charged: 1, reserved: 0, balance: 12 });
const run = `r1-forward-${C[0]}-2`;
book.add("runPlanned", { run, purpose: "forward", round: 1, object: C[0]!, definition });
assert.throws(() => nextStep(book.records), /登记了还没有结果:恢复入口应当已经核对过/);
assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { charged: 1, reserved: 1, balance: 11 });
book.add("runStarted", { run });
assert.throws(() => nextStep(book.records), /登记了还没有结果/);
book.add("runResult", { run, process: exited, judged: judged(1.25, "main") });
assert.deepEqual(nextStep(book.records), forward(C[1]!, 1));
recordForward(book, C[1]!, 1, "timeout");
assert.deepEqual(nextStep(book.records), { do: "adopt", round: 1 });
assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { charged: 3, reserved: 0, balance: 10 });
const before = new Board(book.records);
assert.ok(before.runs().filter((r) => r.plan.purpose === "forward").every((r) => !before.runPublic(r)));
book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: null } });
const after = new Board(book.records);
assert.ok(after.runs().filter((r) => r.plan.purpose === "forward").every((r) => after.runPublic(r)));
// 没有采纳:不开解释阶段,直接开收尾(方法.md 4.3 ⑫)。
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-wrapUp", stage: "wrapUp", round: 1, roster: R });
});
test("采纳只有一个因子的候选:不做消融,直接开收尾(方法.md 4.3 ⑫)", () => {
const book = firstVerification(batchTwo(1), true, true);
for (const c of C) recordForward(book, c, 1, "valid");
book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: C[0]! } });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-wrapUp", stage: "wrapUp", round: 1, roster: R });
});
// ---------------------------------------------------------------------------
// ⑬–⑱:消融到收尾、轮末解封、下一轮和研究结束(任务书 17 第 2.2、2.3 节)。预期动作手写,不调模型、Jev 或 Docker。
// ---------------------------------------------------------------------------
const ADOPTED = "r1-c2"; // 两个因子:p1、p2
const CELL1 = "r1-c2-p1";
const CELL2 = "r1-c2-p2";
/** 采纳了两个因子的 r1-c2。 */
function adoptedBook(): Book {
const book = firstVerification(batchTwo(1), true, true);
for (const c of C) recordForward(book, c, 1, "valid");
return book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: ADOPTED } });
}
/** 解释阶段:开、三人各交一份、关。 */
function explained(book: Book): Book {
return stage(book, "r1-explanation", "explanation", R.map((a) => [a, "explanation", null, null]));
}
/** 选格决定。 */
function cellsChosen(book: Book, chosen: string[]): Book {
return book.add("decision", { decision: "cells", round: 1, rule: "valueMinusCost", ruleVersion: 1, params: null, seed: "s2", input: null, output: { chosen } });
}
/** 一次消融实现阶段:subs 是 [格, cannotRun, alsoOff]。 */
function implStage(book: Book, n: number, subs: Array<[string, string | null, string[]?]>): Book {
const stageId = `r1-ablationImplementation-${n}`;
book.open(stageId, "ablationImplementation", 1, subs.map(([cell]) => `experimenter-${cell}`));
for (const [cell, cannotRun, alsoOff] of subs) {
book.add("submission", { stageId, author: `experimenter-${cell}`, kind: "ablationImplementation", target: cell, item: null, version: 1, content: { alsoOff: alsoOff ?? [], cannotRun, concerns: "无" }, jev: null });
}
return book.add("stageClose", { stageId });
}
/** 一次消融核验阶段:subs 是 [格, [因子, 关没关]]。 */
function verStage(book: Book, n: number, subs: Array<[string, Array<[string, boolean]>]>): Book {
const stageId = `r1-ablationVerification-${n}`;
book.open(stageId, "ablationVerification", 1, subs.map(([cell]) => `verifier-${cell}`));
for (const [cell, offs] of subs) {
book.add("submission", { stageId, author: `verifier-${cell}`, kind: "ablationVerification", target: cell, item: null, version: 1, content: { factors: offs.map(([factor, off]) => ({ factor, off, where: "initial.py:1" })) }, jev: null });
}
return book.add("stageClose", { stageId });
}
/** 一次消融运行:没启动,或启动了、判分算数。 */
function recordAblation(book: Book, cell: string, n: number, ending: "valid" | "notStarted"): Book {
const run = `r1-ablation-${cell}-${n}`;
book.add("runPlanned", { run, purpose: "ablation", round: 1, object: cell, definition });
if (ending === "notStarted") return book.add("runResult", { run, process: { ending, reason: "Docker 未启动" }, judged: null });
book.add("runStarted", { run });
return book.add("runResult", { run, process: exited, judged: judged(1.2, "main") });
}
test("采纳多因子改法:先开解释阶段,每人一个任务、没有对象;关闭后选格", () => {
const book = adoptedBook();
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-explanation", stage: "explanation", round: 1, roster: R });
book.open("r1-explanation", "explanation", 1, R);
assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-explanation", tasks: R.map((actor) => ({ actor, kind: "explanation", object: null })) });
for (const a of R) book.sub("r1-explanation", a, "explanation", null, null);
assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-explanation" });
book.add("stageClose", { stageId: "r1-explanation" });
assert.deepEqual(nextStep(book.records), { do: "selectCells", round: 1 });
});
test("选格做完:每格固定一位实验员;交了跑不起来的格不核验、不运行,没启动的下次起新编号", () => {
const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]);
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-ablationImplementation-1", stage: "ablationImplementation", round: 1, roster: [`experimenter-${CELL1}`, `experimenter-${CELL2}`] });
implStage(book, 1, [[CELL1, null], [CELL2, "离了 p2 跑不起来"]]);
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-ablationVerification-1", stage: "ablationVerification", round: 1, roster: [`verifier-${CELL1}`] });
verStage(book, 1, [[CELL1, [["p1", true], ["p2", false]]]]);
assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL1, run: `r1-ablation-${CELL1}-1` });
recordAblation(book, CELL1, 1, "notStarted");
assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL1, run: `r1-ablation-${CELL1}-2` });
recordAblation(book, CELL1, 2, "valid");
assert.deepEqual(nextStep(book.records), { do: "settle", round: 1 });
});
test("核验不通过:退回原实验员重做,只重验重做的格;次数用完写不跑事件,不登记运行", () => {
const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]);
implStage(book, 1, [[CELL1, null], [CELL2, null]]);
verStage(book, 1, [[CELL1, [["p1", false], ["p2", false]]], [CELL2, [["p1", false], ["p2", true]]]]);
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-ablationImplementation-2", stage: "ablationImplementation", round: 1, roster: [`experimenter-${CELL1}`] });
implStage(book, 2, [[CELL1, null]]);
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-ablationVerification-2", stage: "ablationVerification", round: 1, roster: [`verifier-${CELL1}`] });
verStage(book, 2, [[CELL1, [["p1", false], ["p2", false]]]]);
const next = nextStep(book.records);
assert.equal(next.do, "cellNotRun");
if (next.do !== "cellNotRun") throw new Error("应写不跑事件");
assert.equal(next.cell, CELL1);
assert.match(next.reason, /第 2 次核验仍不通过,修复次数上限 1 已用完/);
book.add("event", { event: "cellNotRun", reason: next.reason, objects: [next.cell] });
// p1 不跑不扣次;只跑 p2。
assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL2, run: `r1-ablation-${CELL2}-1` });
});
test("几格实际关掉的因子完全相同:沿用第一格,不重复跑", () => {
const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]);
implStage(book, 1, [[CELL1, null, ["p2"]], [CELL2, null, ["p1"]]]);
verStage(book, 1, [[CELL1, [["p1", true], ["p2", true]]], [CELL2, [["p1", true], ["p2", true]]]]);
// 两格实际关的都是 {p1, p2}:只跑第一格。
assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL1, run: `r1-ablation-${CELL1}-1` });
recordAblation(book, CELL1, 1, "valid");
assert.deepEqual(nextStep(book.records), { do: "settle", round: 1 });
});
test("全部交跑不起来:不核验、不运行,直接结算", () => {
const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]);
implStage(book, 1, [[CELL1, "跑不起来一"], [CELL2, "跑不起来二"]]);
assert.deepEqual(nextStep(book.records), { do: "settle", round: 1 });
});
/** 结算和押的因子写完、收尾关闭、解封也写完的账本(余额还有 8 次)。 */
function afterWrapUp(): Book {
const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]);
implStage(book, 1, [[CELL1, null], [CELL2, null]]);
verStage(book, 1, [[CELL1, [["p1", true], ["p2", false]]], [CELL2, [["p1", false], ["p2", true]]]]);
recordAblation(book, CELL1, 1, "valid");
recordAblation(book, CELL2, 1, "valid");
book.add("settlement", { round: 1, result: { candidate: ADOPTED, input: null, output: null } });
book.add("decision", { decision: "bet", round: 1, rule: "chooseBet", ruleVersion: 1, params: null, seed: null, input: null, output: null });
return stage(book, "r1-wrapUp", "wrapUp", R.map((a) => [a, "wrapUp", null, null]));
}
test("结算和押的因子写完:开收尾;收尾关闭后解封本轮选中的全部候选,然后开下一轮", () => {
const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]);
implStage(book, 1, [[CELL1, null], [CELL2, null]]);
verStage(book, 1, [[CELL1, [["p1", true], ["p2", false]]], [CELL2, [["p1", false], ["p2", true]]]]);
recordAblation(book, CELL1, 1, "valid");
assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL2, run: `r1-ablation-${CELL2}-1` });
recordAblation(book, CELL2, 1, "valid");
assert.deepEqual(nextStep(book.records), { do: "settle", round: 1 });
book.add("settlement", { round: 1, result: { candidate: ADOPTED, input: null, output: null } });
book.add("decision", { decision: "bet", round: 1, rule: "chooseBet", ruleVersion: 1, params: null, seed: null, input: null, output: null });
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-wrapUp", stage: "wrapUp", round: 1, roster: R });
stage(book, "r1-wrapUp", "wrapUp", R.map((a) => [a, "wrapUp", null, null]));
// 收尾关闭后:解封本轮选批选中的 r1-c1、r1-c2;还有一个没解封就继续。
assert.deepEqual(nextStep(book.records), { do: "unsealChosen", round: 1 });
book.add("unseal", { candidate: "r1-c1", seals: [1] });
assert.deepEqual(nextStep(book.records), { do: "unsealChosen", round: 1 });
book.add("unseal", { candidate: "r1-c2", seals: [2] });
// 余额还有(已扣 5 次):开第 2 轮。
assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r2-proposal", stage: "proposal", round: 2, roster: R });
});
test("余额为 0:不开下一轮,走到导出停下,原因写明", () => {
const book = afterWrapUp();
book.add("unseal", { candidate: "r1-c1", seals: [1] });
book.add("unseal", { candidate: "r1-c2", seals: [2] });
// 已扣 5 次(基线 1、正向 2、消融 2):把冻结配置的运行次数改成 5,余额为 0。
const record = book.records.find((r) => r.kind === "config")!;
const study = record.content as unknown as StudyRecord;
study.config = { ...fullConfig(), runs: 5 } as unknown as Json;
assert.deepEqual(nextStep(book.records), { do: "halt", reason: "导出还没写", objects: [] });
});
test("配置关掉消融、又采纳了多因子改法:先记按配置跳过,再在解释停下", () => {
const book = firstVerification(batchTwo(1), true, true);
for (const c of C) recordForward(book, c, 1, "valid");
book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: ADOPTED } });
// 冻结的配置改为关掉消融:配置和步骤清单一起换(步骤清单在研究开始时冻结)。
const config = fullConfig();
if (config.flow.preset !== "full") throw new Error("测试配置应是完整流程");
// 配置关掉消融时不写 cellRule(它只有开着消融才用得到)。
config.flow.ablation = { enabled: false };
const record = book.records.find((r) => r.kind === "config")!;
const study = record.content as unknown as StudyRecord;
study.config = config as unknown as Json;
study.steps = stepList(config) as unknown as Json;
const reason = "配置关掉了消融";
assert.deepEqual(nextStep(book.records), { do: "skip", step: "r1-listCells", reason });
book.add("event", { event: "skippedByConfig", reason, objects: ["r1-listCells"] });
const next = nextStep(book.records);
assert.equal(next.do, "halt");
assert.match((next as { reason: string }).reason, /不交三个数的解释还没写/);
});